diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml index ddf64463e6..2795869d2c 100644 --- a/.github/workflows/assemble.yml +++ b/.github/workflows/assemble.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/check_release.yml b/.github/workflows/check_release.yml index fa4bcfcea2..fedb53cce3 100644 --- a/.github/workflows/check_release.yml +++ b/.github/workflows/check_release.yml @@ -20,7 +20,7 @@ jobs: # Setup Java 17 # https://github.com/marketplace/actions/setup-java-jdk - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/code_analysis.yml b/.github/workflows/code_analysis.yml index a9f947c96f..552daf7cd8 100644 --- a/.github/workflows/code_analysis.yml +++ b/.github/workflows/code_analysis.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml index a9785155b0..0aaf928ebf 100644 --- a/.github/workflows/publish_docs.yml +++ b/.github/workflows/publish_docs.yml @@ -19,7 +19,7 @@ jobs: # Setup Java 17 # https://github.com/marketplace/actions/setup-java-jdk - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 7fe8eaeb22..0ee9a8f674 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -17,7 +17,7 @@ jobs: ref: main - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 5220997b56..28e21ff83d 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 @@ -37,7 +37,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/sonar_cloud.yml b/.github/workflows/sonar_cloud.yml index e6f9d7acd6..4a4bf2d4ec 100644 --- a/.github/workflows/sonar_cloud.yml +++ b/.github/workflows/sonar_cloud.yml @@ -14,7 +14,7 @@ jobs: fetch-depth: '0' - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/.github/workflows/update_verification_metadata.yml b/.github/workflows/update_verification_metadata.yml index ece0fc2ff0..a14782b84d 100644 --- a/.github/workflows/update_verification_metadata.yml +++ b/.github/workflows/update_verification_metadata.yml @@ -14,7 +14,7 @@ jobs: ref: ${{ github.head_ref }} - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 17 diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Component.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Component.kt index 8310ddfdd7..30ec5a1536 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Component.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Component.kt @@ -21,8 +21,8 @@ import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler import com.adyen.checkout.components.core.internal.IntentHandlingComponent import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent import kotlinx.coroutines.flow.Flow @@ -77,12 +77,11 @@ class Adyen3DS2Component internal constructor( override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } delegate.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER: ActionComponentProvider = diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Configuration.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Configuration.kt index 1315698c9d..ab4fcfbf18 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Configuration.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/Adyen3DS2Configuration.kt @@ -11,8 +11,10 @@ import android.content.Context import android.content.IntentFilter import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.threeds2.customization.UiCustomization import com.adyen.threeds2.internal.ui.activity.ChallengeActivity @@ -25,7 +27,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class Adyen3DS2Configuration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -43,6 +45,22 @@ class Adyen3DS2Configuration private constructor( private var threeDSRequestorAppURL: String? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -50,14 +68,15 @@ class Adyen3DS2Configuration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -66,7 +85,7 @@ class Adyen3DS2Configuration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -106,3 +125,34 @@ class Adyen3DS2Configuration private constructor( } } } + +fun CheckoutConfiguration.adyen3DS2( + configuration: @CheckoutConfigurationMarker Adyen3DS2Configuration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = Adyen3DS2Configuration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addActionConfiguration(config) + return this +} + +fun CheckoutConfiguration.getAdyen3DS2Configuration(): Adyen3DS2Configuration? { + return getActionConfiguration(Adyen3DS2Configuration::class.java) +} + +internal fun Adyen3DS2Configuration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addActionConfiguration(this@toCheckoutConfiguration) + } +} diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintRepository.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintRepository.kt index 99c85911d7..02b443e74a 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintRepository.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintRepository.kt @@ -12,8 +12,8 @@ import com.adyen.checkout.adyen3ds2.internal.data.model.SubmitFingerprintRequest import com.adyen.checkout.adyen3ds2.internal.data.model.SubmitFingerprintResult import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.action.Threeds2Action -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching import org.json.JSONObject @@ -26,37 +26,39 @@ internal class SubmitFingerprintRepository internal constructor( clientKey: String, paymentData: String? ): Result = runSuspendCatching { - Logger.d(TAG, "Submitting fingerprint automatically") + adyenLog(AdyenLogLevel.DEBUG) { "Submitting fingerprint automatically" } val request = SubmitFingerprintRequest( encodedFingerprint = encodedFingerprint, - paymentData = paymentData + paymentData = paymentData, ) val response = submitFingerprintService.submitFingerprint(request, clientKey) when { response.type == RESPONSE_TYPE_COMPLETED && response.details != null -> { - Logger.d(TAG, "submitFingerprint: challenge completed") + adyenLog(AdyenLogLevel.DEBUG) { "submitFingerprint: challenge completed" } SubmitFingerprintResult.Completed(JSONObject(response.details)) } + response.type == RESPONSE_TYPE_ACTION && response.action is RedirectAction -> { - Logger.d(TAG, "submitFingerprint: received new RedirectAction") + adyenLog(AdyenLogLevel.DEBUG) { "submitFingerprint: received new RedirectAction" } SubmitFingerprintResult.Redirect(response.action) } + response.type == RESPONSE_TYPE_ACTION && response.action is Threeds2Action -> { - Logger.d(TAG, "submitFingerprint: received new Threeds2Action") + adyenLog(AdyenLogLevel.DEBUG) { "submitFingerprint: received new Threeds2Action" } SubmitFingerprintResult.Threeds2(response.action) } + else -> { - Logger.e(TAG, "submitFingerprint: unexpected response $response") + adyenLog(AdyenLogLevel.DEBUG) { "submitFingerprint: unexpected response $response" } error("Failed to retrieve 3DS2 fingerprint result") } } } companion object { - private val TAG = LogUtil.getTag() private const val RESPONSE_TYPE_COMPLETED = "completed" private const val RESPONSE_TYPE_ACTION = "action" } diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintService.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintService.kt index 9b244b10e9..104047ee34 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintService.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/data/api/SubmitFingerprintService.kt @@ -12,23 +12,25 @@ import com.adyen.checkout.adyen3ds2.internal.data.model.SubmitFingerprintRequest import com.adyen.checkout.adyen3ds2.internal.data.model.SubmitFingerprintResponse import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.post +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext internal class SubmitFingerprintService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { suspend fun submitFingerprint( request: SubmitFingerprintRequest, clientKey: String - ): SubmitFingerprintResponse = withContext(Dispatchers.IO) { + ): SubmitFingerprintResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/submitThreeDS2Fingerprint", queryParameters = mapOf("token" to clientKey), body = request, requestSerializer = SubmitFingerprintRequest.SERIALIZER, - responseSerializer = SubmitFingerprintResponse.SERIALIZER + responseSerializer = SubmitFingerprintResponse.SERIALIZER, ) } } diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/provider/Adyen3DS2ComponentProvider.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/provider/Adyen3DS2ComponentProvider.kt index 4fab618e76..dc48488c83 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/provider/Adyen3DS2ComponentProvider.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/provider/Adyen3DS2ComponentProvider.kt @@ -23,7 +23,9 @@ import com.adyen.checkout.adyen3ds2.internal.data.model.Adyen3DS2Serializer import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2Delegate import com.adyen.checkout.adyen3ds2.internal.ui.DefaultAdyen3DS2Delegate import com.adyen.checkout.adyen3ds2.internal.ui.model.Adyen3DS2ComponentParamsMapper +import com.adyen.checkout.adyen3ds2.toCheckoutConfiguration import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.Threeds2Action import com.adyen.checkout.components.core.action.Threeds2ChallengeAction @@ -32,12 +34,13 @@ import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.DefaultActionComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentDataRepository import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.AndroidBase64Encoder import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.ui.core.internal.DefaultRedirectHandler import com.adyen.threeds2.ThreeDS2Service import kotlinx.coroutines.Dispatchers @@ -45,27 +48,25 @@ import kotlinx.coroutines.Dispatchers class Adyen3DS2ComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : ActionComponentProvider { - private val componentParamsMapper = Adyen3DS2ComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, application: Application, - configuration: Adyen3DS2Configuration, + checkoutConfiguration: CheckoutConfiguration, callback: ActionComponentCallback, - key: String?, + key: String? ): Adyen3DS2Component { val threeDS2Factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val adyen3DS2Delegate = getDelegate(configuration, savedStateHandle, application) + val adyen3DS2Delegate = getDelegate(checkoutConfiguration, savedStateHandle, application) Adyen3DS2Component( delegate = adyen3DS2Delegate, - actionComponentEventHandler = DefaultActionComponentEventHandler(callback) + actionComponentEventHandler = DefaultActionComponentEventHandler(callback), ) } return ViewModelProvider(viewModelStoreOwner, threeDS2Factory)[key, Adyen3DS2Component::class.java] @@ -75,14 +76,17 @@ constructor( } override fun getDelegate( - configuration: Adyen3DS2Configuration, + checkoutConfiguration: CheckoutConfiguration, savedStateHandle: SavedStateHandle, - application: Application, + application: Application ): Adyen3DS2Delegate { - val componentParams = componentParamsMapper.mapToParams( - adyen3DS2Configuration = configuration, - sessionParams = null, + val componentParams = Adyen3DS2ComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val submitFingerprintService = SubmitFingerprintService(httpClient) val submitFingerprintRepository = SubmitFingerprintRepository(submitFingerprintService) @@ -98,17 +102,37 @@ constructor( adyen3DS2Serializer = adyen3DS2DetailsParser, redirectHandler = redirectHandler, threeDS2Service = ThreeDS2Service.INSTANCE, - defaultDispatcher = Dispatchers.Default, + coroutineDispatcher = Dispatchers.Default, base64Encoder = AndroidBase64Encoder(), application = application, ) } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + configuration: Adyen3DS2Configuration, + callback: ActionComponentCallback, + key: String?, + ): Adyen3DS2Component { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + application = application, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + key = key, + ) + } + override val supportedActionTypes: List get() = listOf( Threeds2FingerprintAction.ACTION_TYPE, Threeds2ChallengeAction.ACTION_TYPE, - Threeds2Action.ACTION_TYPE + Threeds2Action.ACTION_TYPE, ) override fun canHandleAction(action: Action): Boolean { diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2Delegate.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2Delegate.kt index ecd48c3b4a..cc5295a438 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2Delegate.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2Delegate.kt @@ -36,10 +36,11 @@ import com.adyen.checkout.components.core.internal.SavedStateHandleContainer import com.adyen.checkout.components.core.internal.SavedStateHandleProperty import com.adyen.checkout.components.core.internal.util.Base64Encoder import com.adyen.checkout.components.core.internal.util.bufferedChannel +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.RedirectHandler import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.threeds2.AuthenticationRequestParameters @@ -48,10 +49,10 @@ import com.adyen.threeds2.ChallengeStatusHandler import com.adyen.threeds2.ThreeDS2Service import com.adyen.threeds2.Transaction import com.adyen.threeds2.exception.InvalidInputException -import com.adyen.threeds2.exception.SDKAlreadyInitializedException import com.adyen.threeds2.exception.SDKNotInitializedException import com.adyen.threeds2.exception.SDKRuntimeException import com.adyen.threeds2.parameters.ChallengeParameters +import com.adyen.threeds2.parameters.ConfigParameters import com.adyen.threeds2.util.AdyenConfigParameters import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineExceptionHandler @@ -74,7 +75,7 @@ internal class DefaultAdyen3DS2Delegate( private val adyen3DS2Serializer: Adyen3DS2Serializer, private val redirectHandler: RedirectHandler, private val threeDS2Service: ThreeDS2Service, - private val defaultDispatcher: CoroutineDispatcher, + private val coroutineDispatcher: CoroutineDispatcher, private val base64Encoder: Base64Encoder, private val application: Application, ) : Adyen3DS2Delegate, ChallengeStatusHandler, SavedStateHandleContainer { @@ -106,9 +107,10 @@ internal class DefaultAdyen3DS2Delegate( observerRepository.addObservers( detailsFlow = detailsFlow, exceptionFlow = exceptionFlow, + permissionFlow = null, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -116,7 +118,6 @@ internal class DefaultAdyen3DS2Delegate( observerRepository.removeObservers() } - @Suppress("ReturnCount") override fun handleAction(action: Action, activity: Activity) { if (action !is BaseThreeds2Action) { exceptionChannel.trySend(ComponentException("Unsupported action")) @@ -126,41 +127,54 @@ internal class DefaultAdyen3DS2Delegate( val paymentData = action.paymentData paymentDataRepository.paymentData = paymentData when (action) { - is Threeds2FingerprintAction -> { - if (action.token.isNullOrEmpty()) { - exceptionChannel.trySend(ComponentException("Fingerprint token not found.")) - return - } - identifyShopper( - activity = activity, - encodedFingerprintToken = action.token.orEmpty(), - submitFingerprintAutomatically = false, - ) - } + is Threeds2FingerprintAction -> handleThreeds2FingerprintAction(action, activity) + is Threeds2ChallengeAction -> handleThreeds2ChallengeAction(action, activity) + is Threeds2Action -> handleThreeds2Action(action, activity) + } + } - is Threeds2ChallengeAction -> { - if (action.token.isNullOrEmpty()) { - exceptionChannel.trySend(ComponentException("Challenge token not found.")) - return - } - challengeShopper(activity, action.token.orEmpty()) - } + private fun handleThreeds2FingerprintAction( + action: Threeds2FingerprintAction, + activity: Activity, + ) { + if (action.token.isNullOrEmpty()) { + exceptionChannel.trySend(ComponentException("Fingerprint token not found.")) + return + } + identifyShopper( + activity = activity, + encodedFingerprintToken = action.token.orEmpty(), + submitFingerprintAutomatically = false, + ) + } - is Threeds2Action -> { - if (action.token.isNullOrEmpty()) { - exceptionChannel.trySend(ComponentException("3DS2 token not found.")) - return - } - if (action.subtype == null) { - exceptionChannel.trySend(ComponentException("3DS2 Action subtype not found.")) - return - } - val subtype = Threeds2Action.SubType.parse(action.subtype.orEmpty()) - // We need to keep authorizationToken in memory to access it later when the 3DS2 challenge is done - authorizationToken = action.authorisationToken - handleActionSubtype(activity, subtype, action.token.orEmpty()) - } + private fun handleThreeds2ChallengeAction( + action: Threeds2ChallengeAction, + activity: Activity, + ) { + if (action.token.isNullOrEmpty()) { + exceptionChannel.trySend(ComponentException("Challenge token not found.")) + return } + challengeShopper(activity, action.token.orEmpty()) + } + + private fun handleThreeds2Action( + action: Threeds2Action, + activity: Activity, + ) { + if (action.token.isNullOrEmpty()) { + exceptionChannel.trySend(ComponentException("3DS2 token not found.")) + return + } + if (action.subtype == null) { + exceptionChannel.trySend(ComponentException("3DS2 Action subtype not found.")) + return + } + val subtype = Threeds2Action.SubType.parse(action.subtype.orEmpty()) + // We need to keep authorizationToken in memory to access it later when the 3DS2 challenge is done + authorizationToken = action.authorisationToken + handleActionSubtype(activity, subtype, action.token.orEmpty()) } private fun handleActionSubtype( @@ -179,76 +193,43 @@ internal class DefaultAdyen3DS2Delegate( } } - @Suppress("LongMethod") @VisibleForTesting - @Throws(ComponentException::class) internal fun identifyShopper( activity: Activity, encodedFingerprintToken: String, submitFingerprintAutomatically: Boolean, ) { - Logger.d(TAG, "identifyShopper - submitFingerprintAutomatically: $submitFingerprintAutomatically") - val decodedFingerprintToken = base64Encoder.decode(encodedFingerprintToken) + adyenLog(AdyenLogLevel.DEBUG) { + "identifyShopper - submitFingerprintAutomatically: $submitFingerprintAutomatically" + } - val fingerprintJson: JSONObject = try { - JSONObject(decodedFingerprintToken) - } catch (e: JSONException) { - throw ComponentException("JSON parsing of FingerprintToken failed", e) + val fingerprintToken = try { + decodeFingerprintToken(encodedFingerprintToken) + } catch (e: CheckoutException) { + exceptionChannel.trySend(ComponentException("Failed to decode fingerprint token", e)) + return } - val fingerprintToken = FingerprintToken.SERIALIZER.deserialize(fingerprintJson) - val configParameters = AdyenConfigParameters.Builder( - // directoryServerId - fingerprintToken.directoryServerId, - // directoryServerPublicKey - fingerprintToken.directoryServerPublicKey, - // directoryServerRootCertificates - fingerprintToken.directoryServerRootCertificates, - ) - .deviceParameterBlockList(componentParams.deviceParameterBlockList) - .build() + val configParameters = createAdyenConfigParameters(fingerprintToken) val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable -> - Logger.e(TAG, "Unexpected uncaught 3DS2 Exception", throwable) + adyenLog(AdyenLogLevel.ERROR, throwable) { "Unexpected uncaught 3DS2 Exception" } exceptionChannel.trySend(CheckoutException("Unexpected 3DS2 exception.", throwable)) } - coroutineScope.launch(defaultDispatcher + coroutineExceptionHandler) { + coroutineScope.launch(coroutineDispatcher + coroutineExceptionHandler) { // This makes sure the 3DS2 SDK doesn't re-use any state from previous transactions closeTransaction() - @Suppress("SwallowedException") try { - Logger.d(TAG, "initialize 3DS2 SDK") + adyenLog(AdyenLogLevel.DEBUG) { "initialize 3DS2 SDK" } threeDS2Service.initialize(activity, configParameters, null, componentParams.uiCustomization) } catch (e: SDKRuntimeException) { exceptionChannel.trySend(ComponentException("Failed to initialize 3DS2 SDK", e)) return@launch - } catch (e: SDKAlreadyInitializedException) { - // This shouldn't cause any side effect. - Logger.w(TAG, "3DS2 Service already initialized.") } - currentTransaction = try { - Logger.d(TAG, "create transaction") - if (fingerprintToken.threeDSMessageVersion != null) { - threeDS2Service.createTransaction(null, fingerprintToken.threeDSMessageVersion) - } else { - exceptionChannel.trySend( - ComponentException( - "Failed to create 3DS2 Transaction. Missing " + - "threeDSMessageVersion inside fingerprintToken." - ) - ) - return@launch - } - } catch (e: SDKNotInitializedException) { - exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e)) - return@launch - } catch (e: SDKRuntimeException) { - exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e)) - return@launch - } + currentTransaction = createTransaction(fingerprintToken) ?: return@launch val authenticationRequestParameters = currentTransaction?.authenticationRequestParameters if (authenticationRequestParameters == null) { @@ -256,6 +237,7 @@ internal class DefaultAdyen3DS2Delegate( return@launch } val encodedFingerprint = createEncodedFingerprint(authenticationRequestParameters) + if (submitFingerprintAutomatically) { submitFingerprintAutomatically(activity, encodedFingerprint) } else { @@ -264,6 +246,54 @@ internal class DefaultAdyen3DS2Delegate( } } + @Throws(ComponentException::class, ModelSerializationException::class) + private fun decodeFingerprintToken(encoded: String): FingerprintToken { + val decodedFingerprintToken = base64Encoder.decode(encoded) + + val fingerprintJson: JSONObject = try { + JSONObject(decodedFingerprintToken) + } catch (e: JSONException) { + throw ComponentException("JSON parsing of FingerprintToken failed", e) + } + + return FingerprintToken.SERIALIZER.deserialize(fingerprintJson) + } + + private fun createAdyenConfigParameters( + fingerprintToken: FingerprintToken + ): ConfigParameters = AdyenConfigParameters.Builder( + // directoryServerId + fingerprintToken.directoryServerId, + // directoryServerPublicKey + fingerprintToken.directoryServerPublicKey, + // directoryServerRootCertificates + fingerprintToken.directoryServerRootCertificates, + ) + .deviceParameterBlockList(componentParams.deviceParameterBlockList) + .build() + + private fun createTransaction(fingerprintToken: FingerprintToken): Transaction? { + if (fingerprintToken.threeDSMessageVersion == null) { + exceptionChannel.trySend( + ComponentException( + "Failed to create 3DS2 Transaction. Missing threeDSMessageVersion inside fingerprintToken.", + ), + ) + return null + } + + return try { + adyenLog(AdyenLogLevel.DEBUG) { "create transaction" } + threeDS2Service.createTransaction(null, fingerprintToken.threeDSMessageVersion) + } catch (e: SDKNotInitializedException) { + exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e)) + null + } catch (e: SDKRuntimeException) { + exceptionChannel.trySend(ComponentException("Failed to create 3DS2 Transaction", e)) + null + } + } + @Throws(ComponentException::class) private fun createEncodedFingerprint(authenticationRequestParameters: AuthenticationRequestParameters): String { return try { @@ -291,11 +321,11 @@ internal class DefaultAdyen3DS2Delegate( submitFingerprintRepository.submitFingerprint( encodedFingerprint, componentParams.clientKey, - paymentDataRepository.paymentData + paymentDataRepository.paymentData, ) .fold( onSuccess = { result -> onSubmitFingerprintResult(result, activity) }, - onFailure = { e -> exceptionChannel.trySend(ComponentException("Unable to submit fingerprint", e)) } + onFailure = { e -> exceptionChannel.trySend(ComponentException("Unable to submit fingerprint", e)) }, ) } @@ -331,7 +361,7 @@ internal class DefaultAdyen3DS2Delegate( private fun makeRedirect(activity: Activity, action: RedirectAction) { val url = action.url try { - Logger.d(TAG, "makeRedirect - $url") + adyenLog(AdyenLogLevel.DEBUG) { "makeRedirect - $url" } redirectHandler.launchUriRedirect(activity, url) } catch (e: CheckoutException) { exceptionChannel.trySend(e) @@ -339,13 +369,12 @@ internal class DefaultAdyen3DS2Delegate( } @VisibleForTesting - @Throws(ComponentException::class) internal fun challengeShopper(activity: Activity, encodedChallengeToken: String) { - Logger.d(TAG, "challengeShopper") + adyenLog(AdyenLogLevel.DEBUG) { "challengeShopper" } if (currentTransaction == null) { exceptionChannel.trySend( - Authentication3DS2Exception("Failed to make challenge, missing reference to initial transaction.") + Authentication3DS2Exception("Failed to make challenge, missing reference to initial transaction."), ) return } @@ -391,7 +420,7 @@ internal class DefaultAdyen3DS2Delegate( } private fun onCompleted(transactionStatus: String) { - Logger.d(TAG, "challenge completed") + adyenLog(AdyenLogLevel.DEBUG) { "challenge completed" } try { val details = makeDetails(transactionStatus) emitDetails(details) @@ -403,13 +432,13 @@ internal class DefaultAdyen3DS2Delegate( } private fun onCancelled() { - Logger.d(TAG, "challenge cancelled") + adyenLog(AdyenLogLevel.DEBUG) { "challenge cancelled" } exceptionChannel.trySend(Cancelled3DS2Exception("Challenge canceled.")) closeTransaction() } private fun onTimeout(result: ChallengeResult.Timeout) { - Logger.d(TAG, "challenge timed out") + adyenLog(AdyenLogLevel.DEBUG) { "challenge timed out" } try { val details = makeDetails(result.transactionStatus, result.additionalDetails) emitDetails(details) @@ -421,7 +450,7 @@ internal class DefaultAdyen3DS2Delegate( } private fun onError(result: ChallengeResult.Error) { - Logger.d(TAG, "challenge timed out") + adyenLog(AdyenLogLevel.DEBUG) { "challenge timed out" } try { val details = makeDetails(result.transactionStatus, result.additionalDetails) emitDetails(details) @@ -476,20 +505,18 @@ internal class DefaultAdyen3DS2Delegate( return if (token == null) { adyen3DS2Serializer.createChallengeDetails( transactionStatus = transactionStatus, - errorDetails = errorDetails + errorDetails = errorDetails, ) } else { adyen3DS2Serializer.createThreeDsResultDetails( transactionStatus = transactionStatus, errorDetails = errorDetails, - authorisationToken = token + authorisationToken = token, ) } } companion object { - private val TAG = LogUtil.getTag() - private const val AUTHORIZATION_TOKEN_KEY = "authorization_token" private const val DEFAULT_CHALLENGE_TIME_OUT = 10 private const val PROTOCOL_VERSION_2_1_0 = "2.1.0" diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParams.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParams.kt index 670b5cba66..806d7a3117 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParams.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParams.kt @@ -8,21 +8,13 @@ package com.adyen.checkout.adyen3ds2.internal.ui.model -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment import com.adyen.threeds2.customization.UiCustomization -import java.util.Locale internal data class Adyen3DS2ComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, + private val commonComponentParams: CommonComponentParams, val uiCustomization: UiCustomization?, val threeDSRequestorAppURL: String?, val deviceParameterBlockList: Set?, -) : ComponentParams +) : ComponentParams by commonComponentParams diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapper.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapper.kt index c17730ced5..7568dc0f30 100644 --- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapper.kt +++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapper.kt @@ -9,64 +9,39 @@ package com.adyen.checkout.adyen3ds2.internal.ui.model import androidx.annotation.VisibleForTesting -import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.adyen3ds2.getAdyen3DS2Configuration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import java.util.Locale internal class Adyen3DS2ComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - adyen3DS2Configuration: Adyen3DS2Configuration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, ): Adyen3DS2ComponentParams { - return adyen3DS2Configuration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } - - private fun Adyen3DS2Configuration.mapToParamsInternal(): Adyen3DS2ComponentParams { + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + val adyen3ds2Configuration = checkoutConfiguration.getAdyen3DS2Configuration() return Adyen3DS2ComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - uiCustomization = uiCustomization, - threeDSRequestorAppURL = threeDSRequestorAppURL, + commonComponentParams = commonComponentParamsMapperData.commonComponentParams, + uiCustomization = adyen3ds2Configuration?.uiCustomization, + threeDSRequestorAppURL = adyen3ds2Configuration?.threeDSRequestorAppURL, // Hardcoded for now, but in the feature we could make this configurable deviceParameterBlockList = DEVICE_PARAMETER_BLOCK_LIST, ) } - private fun Adyen3DS2ComponentParams.override( - overrideComponentParams: ComponentParams? - ): Adyen3DS2ComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) - } - - private fun Adyen3DS2ComponentParams.override( - sessionParams: SessionParams? = null - ): Adyen3DS2ComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, - ) - } - companion object { private const val PHONE_NUMBER_PARAMETER = "A005" diff --git a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ComponentTest.kt b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ComponentTest.kt index 71c2eca173..daa9052fee 100644 --- a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ComponentTest.kt +++ b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ComponentTest.kt @@ -18,12 +18,10 @@ import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2Delegate import com.adyen.checkout.components.core.action.Threeds2Action import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -37,8 +35,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class Adyen3DS2ComponentTest( @Mock private val adyen3DS2Delegate: Adyen3DS2Delegate, @Mock private val actionComponentEventHandler: ActionComponentEventHandler, @@ -48,8 +45,6 @@ internal class Adyen3DS2ComponentTest( @BeforeEach fun before() { - AdyenLogger.setLogLevel(Logger.NONE) - whenever(adyen3DS2Delegate.viewFlow) doReturn MutableStateFlow(Adyen3DS2ComponentViewType) component = Adyen3DS2Component(adyen3DS2Delegate, actionComponentEventHandler) } diff --git a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ConfigurationTest.kt b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ConfigurationTest.kt new file mode 100644 index 0000000000..e99d443a65 --- /dev/null +++ b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/Adyen3DS2ConfigurationTest.kt @@ -0,0 +1,111 @@ +package com.adyen.checkout.adyen3ds2 + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.threeds2.customization.UiCustomization +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class Adyen3DS2ConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + adyen3DS2 { + val uiCustomization = UiCustomization().apply { + setToolbarTitle("title") + } + setUiCustomization(uiCustomization) + setThreeDSRequestorAppURL("some url") + } + } + + val actual = checkoutConfiguration.getAdyen3DS2Configuration() + + val expected = Adyen3DS2Configuration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setUiCustomization( + UiCustomization().apply { + setToolbarTitle("title") + }, + ) + .setThreeDSRequestorAppURL("some url") + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals( + expected.uiCustomization?.toolbarCustomization?.headerText, + actual?.uiCustomization?.toolbarCustomization?.headerText, + ) + assertEquals(expected.threeDSRequestorAppURL, actual?.threeDSRequestorAppURL) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = Adyen3DS2Configuration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setUiCustomization( + UiCustomization().apply { + setToolbarTitle("title") + }, + ) + .setThreeDSRequestorAppURL("some url") + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actual3DS2Config = actual.getAdyen3DS2Configuration() + assertEquals(config.shopperLocale, actual3DS2Config?.shopperLocale) + assertEquals(config.environment, actual3DS2Config?.environment) + assertEquals(config.clientKey, actual3DS2Config?.clientKey) + assertEquals(config.amount, actual3DS2Config?.amount) + assertEquals(config.analyticsConfiguration, actual3DS2Config?.analyticsConfiguration) + assertEquals( + config.uiCustomization?.toolbarCustomization?.headerText, + actual3DS2Config?.uiCustomization?.toolbarCustomization?.headerText, + ) + assertEquals(config.threeDSRequestorAppURL, actual3DS2Config?.threeDSRequestorAppURL) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2DelegateTest.kt b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2DelegateTest.kt index c2b41c4163..5b57a41917 100644 --- a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2DelegateTest.kt +++ b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/DefaultAdyen3DS2DelegateTest.kt @@ -13,7 +13,6 @@ import android.app.Application import android.content.Intent import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration import com.adyen.checkout.adyen3ds2.Authentication3DS2Exception import com.adyen.checkout.adyen3ds2.Cancelled3DS2Exception import com.adyen.checkout.adyen3ds2.internal.data.api.SubmitFingerprintRepository @@ -21,12 +20,14 @@ import com.adyen.checkout.adyen3ds2.internal.data.model.Adyen3DS2Serializer import com.adyen.checkout.adyen3ds2.internal.data.model.SubmitFingerprintResult import com.adyen.checkout.adyen3ds2.internal.ui.model.Adyen3DS2ComponentParamsMapper import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.action.Threeds2Action import com.adyen.checkout.components.core.action.Threeds2ChallengeAction import com.adyen.checkout.components.core.action.Threeds2FingerprintAction import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.util.JavaBase64Encoder import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException @@ -43,7 +44,8 @@ import com.adyen.threeds2.exception.InvalidInputException import com.adyen.threeds2.exception.SDKRuntimeException import com.adyen.threeds2.parameters.ChallengeParameters import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.json.JSONException import org.json.JSONObject @@ -53,7 +55,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension @@ -65,6 +66,7 @@ import org.mockito.kotlin.whenever import java.io.IOException import java.util.Locale +@OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) internal class DefaultAdyen3DS2DelegateTest( @Mock private val submitFingerprintRepository: SubmitFingerprintRepository, @@ -79,15 +81,15 @@ internal class DefaultAdyen3DS2DelegateTest( private val base64Encoder = JavaBase64Encoder() @BeforeEach - fun beforeEach(dispatcher: TestDispatcher) { + fun setup() { redirectHandler = TestRedirectHandler() paymentDataRepository = PaymentDataRepository(SavedStateHandle()) - val configuration = Adyen3DS2Configuration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build() + val configuration = CheckoutConfiguration(Environment.TEST, TEST_CLIENT_KEY) delegate = DefaultAdyen3DS2Delegate( observerRepository = ActionObserverRepository(), savedStateHandle = SavedStateHandle(), - componentParams = Adyen3DS2ComponentParamsMapper(null, null) - .mapToParams(configuration, null) + componentParams = Adyen3DS2ComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null) // Set it to null to avoid a crash in 3DS2 library (they use Android APIs) .copy(deviceParameterBlockList = null), submitFingerprintRepository = submitFingerprintRepository, @@ -95,7 +97,7 @@ internal class DefaultAdyen3DS2DelegateTest( adyen3DS2Serializer = adyen3DS2Serializer, redirectHandler = redirectHandler, threeDS2Service = threeDS2Service, - defaultDispatcher = dispatcher, + coroutineDispatcher = UnconfinedTestDispatcher(), base64Encoder = base64Encoder, application = Application(), ) @@ -106,10 +108,8 @@ internal class DefaultAdyen3DS2DelegateTest( inner class HandleActionTest { @Test - fun `Threeds2FingerprintAction and token is null, then an exception is thrown`( - dispatcher: TestDispatcher - ) = runTest { - delegate.initialize(CoroutineScope(dispatcher)) + fun `Threeds2FingerprintAction and token is null, then an exception is thrown`() = runTest { + delegate.initialize(this) delegate.exceptionFlow.test { delegate.handleAction(Threeds2FingerprintAction(token = null), Activity()) @@ -119,10 +119,8 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `Threeds2ChallengeAction and token is null, then an exception is thrown`( - dispatcher: TestDispatcher - ) = runTest { - delegate.initialize(CoroutineScope(dispatcher)) + fun `Threeds2ChallengeAction and token is null, then an exception is thrown`() = runTest { + delegate.initialize(this) delegate.exceptionFlow.test { delegate.handleAction(Threeds2ChallengeAction(token = null), Activity()) @@ -132,10 +130,8 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `Threeds2Action and token is null, then an exception is thrown`( - dispatcher: TestDispatcher - ) = runTest { - delegate.initialize(CoroutineScope(dispatcher)) + fun `Threeds2Action and token is null, then an exception is thrown`() = runTest { + delegate.initialize(this) delegate.exceptionFlow.test { delegate.handleAction(Threeds2Action(token = null), Activity()) @@ -145,10 +141,8 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `Threeds2Action and sub type is null, then an exception is thrown`( - dispatcher: TestDispatcher - ) = runTest { - delegate.initialize(CoroutineScope(dispatcher)) + fun `Threeds2Action and sub type is null, then an exception is thrown`() = runTest { + delegate.initialize(this) delegate.exceptionFlow.test { delegate.handleAction(Threeds2Action(token = "sometoken", subtype = null), Activity()) @@ -163,46 +157,47 @@ internal class DefaultAdyen3DS2DelegateTest( inner class IdentifyShopperTest { @Test - fun `fingerprint is malformed, then an exception is thrown`(dispatcher: TestDispatcher) = runTest { - delegate.initialize(CoroutineScope(dispatcher)) + fun `fingerprint is malformed, then an exception is thrown`() = runTest { + delegate.initialize(this) - assertThrows { + delegate.exceptionFlow.test { val encodedJson = base64Encoder.encode("{incorrectJson}") delegate.identifyShopper(Activity(), encodedJson, false) + + assertTrue(awaitItem() is ComponentException) } } @Test - fun `3ds2 sdk throws an exception while initializing, then an exception emitted`(dispatcher: TestDispatcher) = - runTest { - val error = SDKRuntimeException("test", "test", null) - whenever(threeDS2Service.initialize(any(), any(), anyOrNull(), anyOrNull())) doAnswer { - throw error - } - delegate.initialize(CoroutineScope(dispatcher)) - - delegate.exceptionFlow.test { - val encodedJson = base64Encoder.encode( - """ - { - "directoryServerId":"id", - "directoryServerPublicKey":"key" - } - """.trimIndent() - ) - delegate.identifyShopper(Activity(), encodedJson, false) - - assertEquals(error, awaitItem().cause) - } + fun `3ds2 sdk throws an exception while initializing, then an exception emitted`() = runTest { + val error = SDKRuntimeException("test", "test", null) + whenever(threeDS2Service.initialize(any(), any(), anyOrNull(), anyOrNull())) doAnswer { + throw error + } + delegate.initialize(this) + + delegate.exceptionFlow.test { + val encodedJson = base64Encoder.encode( + """ + { + "directoryServerId":"id", + "directoryServerPublicKey":"key" + } + """.trimIndent(), + ) + delegate.identifyShopper(Activity(), encodedJson, false) + + assertEquals(error, awaitItem().cause) } + } @Test - fun `creating 3ds2 transaction fails, then an exception emitted`(dispatcher: TestDispatcher) = runTest { + fun `creating 3ds2 transaction fails, then an exception emitted`() = runTest { val error = SDKRuntimeException("test", "test", null) whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doAnswer { throw error } - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(this) delegate.exceptionFlow.test { val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) @@ -213,9 +208,9 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `transaction parameters are null, then an exception emitted`(dispatcher: TestDispatcher) = runTest { + fun `transaction parameters are null, then an exception emitted`() = runTest { whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction() - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(this) delegate.exceptionFlow.test { val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) @@ -226,41 +221,36 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `fingerprint is submitted automatically and result is completed, then details are emitted`( - dispatcher: TestDispatcher - ) = - runTest { - val authReqParams = TestAuthenticationRequestParameters( - deviceData = "deviceData", - sdkTransactionID = "sdkTransactionID", - sdkAppID = "sdkAppID", - sdkReferenceNumber = "sdkReferenceNumber", - sdkEphemeralPublicKey = "{}", - messageVersion = "messageVersion", - ) - whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction(authReqParams) - val submitFingerprintResult = SubmitFingerprintResult.Completed(JSONObject()) - whenever(submitFingerprintRepository.submitFingerprint(any(), any(), anyOrNull())) doReturn - Result.success(submitFingerprintResult) + fun `fingerprint is submitted automatically and result is completed, then details are emitted`() = runTest { + val authReqParams = TestAuthenticationRequestParameters( + deviceData = "deviceData", + sdkTransactionID = "sdkTransactionID", + sdkAppID = "sdkAppID", + sdkReferenceNumber = "sdkReferenceNumber", + sdkEphemeralPublicKey = "{}", + messageVersion = "messageVersion", + ) + whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction(authReqParams) + val submitFingerprintResult = SubmitFingerprintResult.Completed(JSONObject()) + whenever(submitFingerprintRepository.submitFingerprint(any(), any(), anyOrNull())) doReturn + Result.success(submitFingerprintResult) - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(this) - delegate.detailsFlow.test { - val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) - delegate.identifyShopper(Activity(), encodedJson, true) + delegate.detailsFlow.test { + val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) + delegate.identifyShopper(Activity(), encodedJson, true) - val expected = ActionComponentData( - paymentData = null, - details = submitFingerprintResult.details, - ) - assertEquals(expected, awaitItem()) - } + val expected = ActionComponentData( + paymentData = null, + details = submitFingerprintResult.details, + ) + assertEquals(expected, awaitItem()) } + } @Test - fun `fingerprint is submitted automatically and result is redirect, then redirect should be handled`( - dispatcher: TestDispatcher - ) = + fun `fingerprint is submitted automatically and result is redirect, then redirect should be handled`() = runTest { val authReqParams = TestAuthenticationRequestParameters( deviceData = "deviceData", @@ -275,7 +265,7 @@ internal class DefaultAdyen3DS2DelegateTest( whenever(submitFingerprintRepository.submitFingerprint(any(), any(), anyOrNull())) doReturn Result.success(submitFingerprintResult) - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(this) val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) delegate.identifyShopper(Activity(), encodedJson, true) @@ -284,60 +274,56 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `fingerprint is submitted automatically and it fails, then an exception is emitted`( - dispatcher: TestDispatcher - ) = - runTest { - val authReqParams = TestAuthenticationRequestParameters( - deviceData = "deviceData", - sdkTransactionID = "sdkTransactionID", - sdkAppID = "sdkAppID", - sdkReferenceNumber = "sdkReferenceNumber", - sdkEphemeralPublicKey = "{}", - messageVersion = "messageVersion", - ) - whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction(authReqParams) - val error = IOException("test") - whenever(submitFingerprintRepository.submitFingerprint(any(), any(), anyOrNull())) doReturn - Result.failure(error) + fun `fingerprint is submitted automatically and it fails, then an exception is emitted`() = runTest { + val authReqParams = TestAuthenticationRequestParameters( + deviceData = "deviceData", + sdkTransactionID = "sdkTransactionID", + sdkAppID = "sdkAppID", + sdkReferenceNumber = "sdkReferenceNumber", + sdkEphemeralPublicKey = "{}", + messageVersion = "messageVersion", + ) + whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction(authReqParams) + val error = IOException("test") + whenever(submitFingerprintRepository.submitFingerprint(any(), any(), anyOrNull())) doReturn + Result.failure(error) - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(this) - delegate.exceptionFlow.test { - val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) - delegate.identifyShopper(Activity(), encodedJson, true) - assertEquals(error, awaitItem().cause) - } + delegate.exceptionFlow.test { + val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) + delegate.identifyShopper(Activity(), encodedJson, true) + assertEquals(error, awaitItem().cause) } + } @Test - fun `fingerprint is not submitted automatically, then details are emitted`(dispatcher: TestDispatcher) = - runTest { - val authReqParams = TestAuthenticationRequestParameters( - deviceData = "deviceData", - sdkTransactionID = "sdkTransactionID", - sdkAppID = "sdkAppID", - sdkReferenceNumber = "sdkReferenceNumber", - sdkEphemeralPublicKey = "{}", - messageVersion = "messageVersion", - ) - whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction(authReqParams) - val fingerprintDetails = JSONObject("{\"finger\":\"print\"}") - whenever(adyen3DS2Serializer.createFingerprintDetails(any())) doReturn fingerprintDetails + fun `fingerprint is not submitted automatically, then details are emitted`() = runTest { + val authReqParams = TestAuthenticationRequestParameters( + deviceData = "deviceData", + sdkTransactionID = "sdkTransactionID", + sdkAppID = "sdkAppID", + sdkReferenceNumber = "sdkReferenceNumber", + sdkEphemeralPublicKey = "{}", + messageVersion = "messageVersion", + ) + whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn TestTransaction(authReqParams) + val fingerprintDetails = JSONObject("{\"finger\":\"print\"}") + whenever(adyen3DS2Serializer.createFingerprintDetails(any())) doReturn fingerprintDetails - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(this) - delegate.detailsFlow.test { - val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) - delegate.identifyShopper(Activity(), encodedJson, false) + delegate.detailsFlow.test { + val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) + delegate.identifyShopper(Activity(), encodedJson, false) - val expected = ActionComponentData( - paymentData = null, - details = fingerprintDetails, - ) - assertEquals(expected, awaitItem()) - } + val expected = ActionComponentData( + paymentData = null, + details = fingerprintDetails, + ) + assertEquals(expected, awaitItem()) } + } } @Nested @@ -354,8 +340,8 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `token can't be decoded, then an exception is emitted`(dispatcher: TestDispatcher) = runTest { - initializeTransaction(dispatcher) + fun `token can't be decoded, then an exception is emitted`() = runTest { + initializeTransaction(this) delegate.exceptionFlow.test { delegate.challengeShopper(Activity(), base64Encoder.encode("token")) @@ -365,8 +351,8 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `everything is good, then challenge should be executed`(dispatcher: TestDispatcher) = runTest { - val transaction = initializeTransaction(dispatcher) + fun `everything is good, then challenge should be executed`() = runTest { + val transaction = initializeTransaction(this) delegate.challengeShopper(Activity(), base64Encoder.encode("{}")) @@ -374,8 +360,8 @@ internal class DefaultAdyen3DS2DelegateTest( } @Test - fun `challenge fails, then an exception is emitted`(dispatcher: TestDispatcher) = runTest { - initializeTransaction(dispatcher).apply { + fun `challenge fails, then an exception is emitted`() = runTest { + initializeTransaction(this).apply { shouldThrowError = true } @@ -386,7 +372,7 @@ internal class DefaultAdyen3DS2DelegateTest( } } - private fun initializeTransaction(dispatcher: TestDispatcher): TestTransaction { + private fun initializeTransaction(scope: CoroutineScope): TestTransaction { val authReqParams = TestAuthenticationRequestParameters( deviceData = "deviceData", sdkTransactionID = "sdkTransactionID", @@ -398,7 +384,7 @@ internal class DefaultAdyen3DS2DelegateTest( val transaction = TestTransaction(authReqParams) whenever(threeDS2Service.createTransaction(anyOrNull(), any())) doReturn transaction - delegate.initialize(CoroutineScope(dispatcher)) + delegate.initialize(scope) val encodedJson = base64Encoder.encode(TEST_FINGERPRINT_TOKEN) delegate.identifyShopper(Activity(), encodedJson, false) @@ -446,15 +432,15 @@ internal class DefaultAdyen3DS2DelegateTest( val details = JSONObject("{}") whenever( adyen3DS2Serializer.createChallengeDetails( - transactionStatus = "transactionStatus" - ) + transactionStatus = "transactionStatus", + ), ) doReturn details delegate.detailsFlow.test { delegate.onCompletion( result = ChallengeResult.Completed( - transactionStatus = "transactionStatus" - ) + transactionStatus = "transactionStatus", + ), ) val expected = ActionComponentData( @@ -470,15 +456,15 @@ internal class DefaultAdyen3DS2DelegateTest( val error = ComponentException("test") whenever( adyen3DS2Serializer.createChallengeDetails( - transactionStatus = "transactionStatus" - ) + transactionStatus = "transactionStatus", + ), ) doAnswer { throw error } delegate.exceptionFlow.test { delegate.onCompletion( result = ChallengeResult.Completed( - transactionStatus = "transactionStatus" - ) + transactionStatus = "transactionStatus", + ), ) assertEquals(error, awaitItem()) @@ -491,8 +477,8 @@ internal class DefaultAdyen3DS2DelegateTest( delegate.onCompletion( result = ChallengeResult.Cancelled( transactionStatus = "transactionStatus", - additionalDetails = "additionalDetails" - ) + additionalDetails = "additionalDetails", + ), ) assertTrue(awaitItem() is Cancelled3DS2Exception) @@ -505,16 +491,16 @@ internal class DefaultAdyen3DS2DelegateTest( whenever( adyen3DS2Serializer.createChallengeDetails( transactionStatus = "transactionStatus", - errorDetails = "additionalDetails" - ) + errorDetails = "additionalDetails", + ), ) doReturn details delegate.detailsFlow.test { delegate.onCompletion( result = ChallengeResult.Timeout( transactionStatus = "transactionStatus", - additionalDetails = "additionalDetails" - ) + additionalDetails = "additionalDetails", + ), ) val expected = ActionComponentData( @@ -531,16 +517,16 @@ internal class DefaultAdyen3DS2DelegateTest( whenever( adyen3DS2Serializer.createChallengeDetails( transactionStatus = "transactionStatus", - errorDetails = "additionalDetails" - ) + errorDetails = "additionalDetails", + ), ) doReturn details delegate.detailsFlow.test { delegate.onCompletion( result = ChallengeResult.Error( transactionStatus = "transactionStatus", - additionalDetails = "additionalDetails" - ) + additionalDetails = "additionalDetails", + ), ) val expected = ActionComponentData( @@ -562,12 +548,8 @@ internal class DefaultAdyen3DS2DelegateTest( override fun getAuthenticationRequestParameters(): AuthenticationRequestParameters? = authReqParameters - override fun doChallenge(p0: Activity?, p1: ChallengeParameters?, p2: ChallengeStatusReceiver?, p3: Int) { - timesDoChallengeCalled++ - if (shouldThrowError) { - throw InvalidInputException("test", null) - } - } + @Suppress("OVERRIDE_DEPRECATION", "deprecation") + override fun doChallenge(p0: Activity?, p1: ChallengeParameters?, p2: ChallengeStatusReceiver?, p3: Int) = Unit override fun doChallenge( currentActivity: Activity?, @@ -579,7 +561,6 @@ internal class DefaultAdyen3DS2DelegateTest( if (shouldThrowError) { throw InvalidInputException("test", null) } - // TODO } override fun getProgressView(p0: Activity?): ProgressDialog { diff --git a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapperTest.kt b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapperTest.kt index 2c0076351f..7b4bd50998 100644 --- a/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapperTest.kt +++ b/3ds2/src/test/java/com/adyen/checkout/adyen3ds2/internal/ui/model/Adyen3DS2ComponentParamsMapperTest.kt @@ -8,26 +8,36 @@ package com.adyen.checkout.adyen3ds2.internal.ui.model -import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration +import com.adyen.checkout.adyen3ds2.adyen3DS2 import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration +import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.threeds2.customization.UiCustomization import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import java.util.Locale internal class Adyen3DS2ComponentParamsMapperTest { + private val adyen3DS2ComponentParamsMapper = Adyen3DS2ComponentParamsMapper(CommonComponentParamsMapper()) + @Test - fun `when parent configuration is null and custom 3ds2 configuration fields are null then all fields should match`() { - val adyen3DS2Configuration = getAdyen3DS2ConfigurationBuilder() - .build() + fun `when drop-in override params are null and custom 3ds2 configuration fields are null then all fields should match`() { + val checkoutConfiguration = getCheckoutConfiguration() - val params = Adyen3DS2ComponentParamsMapper(null, null) - .mapToParams(adyen3DS2Configuration, null) + val params = adyen3DS2ComponentParamsMapper.mapToParams(checkoutConfiguration, DEVICE_LOCALE, null, null) val expected = getAdyen3DS2ComponentParams() @@ -35,17 +45,18 @@ internal class Adyen3DS2ComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom 3ds2 configuration fields are set then all fields should match`() { + fun `when drop-in override params are null and custom 3ds2 configuration fields are set then all fields should match`() { val uiCustomization = UiCustomization() val testUrl = "https://adyen.com" - val adyen3DS2Configuration = getAdyen3DS2ConfigurationBuilder() - .setUiCustomization(uiCustomization) - .setThreeDSRequestorAppURL(testUrl) - .build() + val configuration = getCheckoutConfiguration { + adyen3DS2 { + setUiCustomization(uiCustomization) + setThreeDSRequestorAppURL(testUrl) + } + } - val params = Adyen3DS2ComponentParamsMapper(null, null) - .mapToParams(adyen3DS2Configuration, null) + val params = adyen3DS2ComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAdyen3DS2ComponentParams( uiCustomization = uiCustomization, @@ -56,26 +67,25 @@ internal class Adyen3DS2ComponentParamsMapperTest { } @Test - fun `when parent configuration is set then parent configuration fields should override 3ds2 configuration fields`() { - val adyen3DS2Configuration = getAdyen3DS2ConfigurationBuilder() - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override 3ds2 configuration fields`() { + val checkoutConfiguration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), amount = Amount( currency = "USD", - value = 25_00L - ) + value = 25_00L, + ), ) - val params = Adyen3DS2ComponentParamsMapper(overrideParams, null) - .mapToParams(adyen3DS2Configuration, null) + val dropInOverrideParams = DropInOverrideParams(Amount("CAD", 123L), null) + val params = adyen3DS2ComponentParamsMapper.mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) val expected = getAdyen3DS2ComponentParams( shopperLocale = Locale.GERMAN, @@ -84,23 +94,133 @@ internal class Adyen3DS2ComponentParamsMapperTest { analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "USD", - value = 25_00L + currency = "CAD", + value = 123L, ), ) assertEquals(expected, params) } - private fun getAdyen3DS2ConfigurationBuilder() = Adyen3DS2Configuration.Builder( - shopperLocale = Locale.US, + @ParameterizedTest + @MethodSource("amountSource") + fun `amount should match value set in sessions then drop in then component configuration`( + configurationValue: Amount, + dropInValue: Amount?, + sessionsValue: Amount?, + expectedValue: Amount + ) { + val configuration = getCheckoutConfiguration(amount = configurationValue) + + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + + val params = adyen3DS2ComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + ) + + val expected = getAdyen3DS2ComponentParams( + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = getCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = adyen3DS2ComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getAdyen3DS2ComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = getCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = adyen3DS2ComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getAdyen3DS2ComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun getCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + config: CheckoutConfiguration.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + configurationBlock = config, + ) + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, ) @Suppress("LongParameterList") private fun getAdyen3DS2ComponentParams( - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -109,12 +229,14 @@ internal class Adyen3DS2ComponentParamsMapperTest { uiCustomization: UiCustomization? = null, threeDSRequestorAppURL: String? = null, ) = Adyen3DS2ComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, - amount = amount, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), uiCustomization = uiCustomization, threeDSRequestorAppURL = threeDSRequestorAppURL, deviceParameterBlockList = Adyen3DS2ComponentParamsMapper.DEVICE_PARAMETER_BLOCK_LIST, @@ -123,5 +245,23 @@ internal class Adyen3DS2ComponentParamsMapperTest { companion object { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") + + @JvmStatic + fun amountSource() = listOf( + // configurationValue, dropInValue, sessionsValue, expectedValue + Arguments.arguments(Amount("EUR", 100), Amount("USD", 200), Amount("CAD", 300), Amount("CAD", 300)), + Arguments.arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), + Arguments.arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), + ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + Arguments.arguments(null, null, Locale.US, Locale.US), + Arguments.arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + Arguments.arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + Arguments.arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/README.md b/README.md index 647b5d1fb5..6a55482726 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,23 @@ Import the corresponding module in your `build.gradle` file. For Drop-in: ```groovy -implementation "com.adyen.checkout:drop-in-compose:5.2.0" +implementation "com.adyen.checkout:drop-in-compose:5.3.0" ``` For the Credit Card component: ```groovy -implementation "com.adyen.checkout:card:5.2.0" -implementation "com.adyen.checkout:components-compose:5.2.0" +implementation "com.adyen.checkout:card:5.3.0" +implementation "com.adyen.checkout:components-compose:5.3.0" ``` ### Without Jetpack Compose For Drop-in: ```groovy -implementation "com.adyen.checkout:drop-in:5.2.0" +implementation "com.adyen.checkout:drop-in:5.3.0" ``` For the Credit Card component: ```groovy -implementation "com.adyen.checkout:card:5.2.0" +implementation "com.adyen.checkout:card:5.3.0" ``` The library is available on [Maven Central][mavenRepo]. @@ -65,6 +65,10 @@ If you are upgrading from 4.x.x to a current release, check out our [migration g If you use ProGuard or R8, you do not need to manually add any rules, as they are automatically embedded in the artifacts. Please let us know if you find any issues. +## Development + +For development and testing purposes the project is accompanied by a test app. See [here](example-app/README.md) how to set it up and run it. + ## Support If you have a feature request, or spotted a bug or a technical problem, [create an issue here][github.newIssue]. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a5300f72d8..eb45949724 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -9,22 +9,70 @@ [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) ## New -- We added a [UI customization guide](docs/UI_CUSTOMIZATION.md), which explains how to customize the styles and string resources. +- A new way to create a configuration using DSL to be more declarative and concise: +```Kotlin +CheckoutConfiguration( + environment = environment, + clientKey = clientKey, + shopperLocale = shopperLocale, + amount = amount, +) { + dropIn { + setEnableRemovingStoredPaymentMethods(true) + } + + card { + setHolderNameRequired(true) + setShopperReference("...") + } -## Improved -- The integration now uses JSON Web Encryption (JWE) with RSA OAEP 256 and AES GCM 256 for encryption. You do not need to make any changes to your integration. + adyen3DS2 { + setThreeDSRequestorAppURL("...") + } +} +``` + +- For the Card Component, you can use the new [Address Lookup functionality](docs/ADDRESS_LOOKUP.md). +- For voucher actions: when the `url` or `downloadUrl` is not included, the shopper has the option to select **Save as image** and save the voucher to the device's `Downloads` folder. +- You can now set your own `AdyenLogger` instance with `AdyenLogger.setLogger`. This gives the ability to intercept logs and handle them in your own way. +- [Instructions](example-app/README.md) to use the testing app in the repository. You can follow `How to migrate` section [here](https://github.com/Adyen/adyen-android/pull/1505). +- Payment methods: + - Multibanco. Payment method type: **multibanco**. + - Pay Easy. Payment method type: **econtext_atm**. + - Convenience Stores Japan. Payment method type: **econtext_stores** + - Online Banking Japan. Payment method type: **econtext_online**. + - Seven-Eleven: Payment method type: **econtext_seven_eleven** ## Fixed -- For Drop-in, error dialogs no longer display user unfriendly messages when using the Sessions flow. -- Overriding some of the XML styles without specifying a parent style no longer causes a build error. -- The Await and QR Code action components no longer get stuck in a loading state after the payment is completed. +- When building `minifyEnabled` without the `kotlin-parcelize` plugin in your project, the build should no longer crash. +- When handling actions, you no longer get the `IllegalArgumentException: Unsupported delegate type` error that causes a crash. + +## Deprecated +- When creating a configuration, the `Builder` constructors with a `Context` is deprecated. You can now omit the `context` parameter. +- `PermissionException`. Handle permissions through `ActionComponentCallback`, `SessionComponentCallback`, or `ComponentCallback` callbacks instead. +- The styles for vouchers have been changed: + - | Previous (v5.2.0 or earlier) | Now (v5.3.0) | + |---------------------------------------------|-----------------------------------------------| + | `AdyenCheckout.Voucher.Description.Bacs` | `AdyenCheckout.Voucher.Simple.Description` | + | `AdyenCheckout.Voucher.Description.Boleto` | `AdyenCheckout.Voucher.Full.Description` | + | `AdyenCheckout.Voucher.ExpirationDateLabel` | `AdyenCheckout.Voucher.InformationFieldLabel` | + | `AdyenCheckout.Voucher.ExpirationDate` | `AdyenCheckout.Voucher.InformationFieldValue` | + | `AdyenCheckout.Voucher.ButtonCopyCode` | `AdyenCheckout.Voucher.Button.CopyCode` | + | `AdyenCheckout.Voucher.ButtonDownloadPdf` | `AdyenCheckout.Voucher.Button.DownloadPdf` | +- Logger.LogLevel has been deprecated. + - | Previous (v5.2.0 or earlier) | Now (v5.3.0) | + |------------------------------------------|-------------------------------------------------| + | `Logger.LogLevel` | `AdyenLogLevel` | + | `AdyenLogger.setLogLevel(logLevel: Int)` | `AdyenLogger.setLogLevel(level: AdyenLogLevel)` | ## Changed -- Dependency versions: - | Name | Version | - |--------------------------------------------------------------------------------------------------------|-------------------------------| - | [Kotlin](https://kotlinlang.org/docs/releases.html#release-details) | **1.9.21** | - | [Android Gradle plugin](https://developer.android.com/build/releases/gradle-plugin) | **8.2.0** | - | [AndroidX Compose compiler](https://developer.android.com/jetpack/androidx/releases/compose-compiler) | **1.5.7** | - | [AndroidX Compose Activity](https://developer.android.com/jetpack/androidx/releases/activity#1.8.1) | **1.8.1** | - | [AndroidX Browser](https://developer.android.com/jetpack/androidx/releases/browser#1.7.0) | **1.7.0** | +- When creating a configuration, the `shopperLocale` parameter is now optional. + - Sessions flow: when you don't set it, the shopper locale is set to the value included in the `/sessions` request. + - Advanced flow: when you don't set it, the shopper local is set to the primary user locale on the device. +- For Drop-in, all actions now start in expanded mode. +- For the Google Pay Component, you no longer need to manually import the `3ds2` module to handle transactions that require Native 3D Secure 2 challenge. +- If you use `DropInServiceResult.Error` without specifying an error message, the default has changed from `Error sending payment. Please try again.` to `An unknown error occurred`. +- For the Sessions flow: + - When starting Drop-in (with `DropIn.startPayment`) or creating a Component (with `YourComponent.PROVIDER.get`), the `configuration` parameter is now optional. + - When using `CheckoutSessionProvider.createSession` to create a `CheckoutSession`, you can pass only `environment` and `clientKey` instead of the whole configuration. + - Removing stored payment methods is now handled internally. You no longer need to override the `onRemoveStoredPaymentMethod` function. diff --git a/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitComponent.kt b/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitComponent.kt index 9e7ede9561..64f7238791 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitComponent.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitComponent.kt @@ -24,8 +24,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent @@ -77,24 +77,24 @@ class ACHDirectDebitComponent internal constructor( (achDirectDebitDelegate as? ButtonDelegate)?.isConfirmationRequired() ?: false override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? DefaultACHDirectDebitDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } achDirectDebitDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = ACHDirectDebitComponentProvider() diff --git a/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitConfiguration.kt b/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitConfiguration.kt index 550ab4f8ac..5562fb7af1 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitConfiguration.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitConfiguration.kt @@ -13,9 +13,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -26,7 +29,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class ACHDirectDebitConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -48,6 +51,22 @@ class ACHDirectDebitConfiguration private constructor( private var addressConfiguration: ACHDirectDebitAddressConfiguration? = null private var isStorePaymentFieldVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -55,14 +74,15 @@ class ACHDirectDebitConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -71,7 +91,7 @@ class ACHDirectDebitConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -104,6 +124,10 @@ class ACHDirectDebitConfiguration private constructor( * * Default is true. * + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * * @param showStorePaymentField [Boolean] * @return [ACHDirectDebitConfiguration.Builder] */ @@ -122,8 +146,37 @@ class ACHDirectDebitConfiguration private constructor( isSubmitButtonVisible = isSubmitButtonVisible, genericActionConfiguration = genericActionConfigurationBuilder.build(), addressConfiguration = addressConfiguration, - isStorePaymentFieldVisible = isStorePaymentFieldVisible + isStorePaymentFieldVisible = isStorePaymentFieldVisible, ) } } } + +fun CheckoutConfiguration.achDirectDebit( + configuration: @CheckoutConfigurationMarker ACHDirectDebitConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = ACHDirectDebitConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ACH, config) + return this +} + +fun CheckoutConfiguration.getACHDirectDebitConfiguration(): ACHDirectDebitConfiguration? { + return getConfiguration(PaymentMethodTypes.ACH) +} + +internal fun ACHDirectDebitConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration(environment, clientKey, shopperLocale, amount, analyticsConfiguration) { + addConfiguration(PaymentMethodTypes.ACH, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/provider/ACHDirectDebitComponentProvider.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/provider/ACHDirectDebitComponentProvider.kt index a828293980..410cadd5e3 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/provider/ACHDirectDebitComponentProvider.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/provider/ACHDirectDebitComponentProvider.kt @@ -11,21 +11,27 @@ package com.adyen.checkout.ach.internal.provider import android.app.Application import androidx.annotation.RestrictTo import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.ach.ACHDirectDebitComponent import com.adyen.checkout.ach.ACHDirectDebitComponentState import com.adyen.checkout.ach.ACHDirectDebitConfiguration +import com.adyen.checkout.ach.internal.ui.ACHDirectDebitDelegate import com.adyen.checkout.ach.internal.ui.DefaultACHDirectDebitDelegate import com.adyen.checkout.ach.internal.ui.StoredACHDirectDebitDelegate +import com.adyen.checkout.ach.internal.ui.model.ACHDirectDebitComponentParams import com.adyen.checkout.ach.internal.ui.model.ACHDirectDebitComponentParamsMapper +import com.adyen.checkout.ach.toCheckoutConfiguration import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.DefaultComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper @@ -37,12 +43,14 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultPublicKeyRepo import com.adyen.checkout.components.core.internal.data.api.PublicKeyService import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.provider.StoredPaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException +import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.cse.internal.GenericEncryptorFactory import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback @@ -58,64 +66,61 @@ import com.adyen.checkout.ui.core.internal.data.api.AddressService import com.adyen.checkout.ui.core.internal.data.api.DefaultAddressRepository import com.adyen.checkout.ui.core.internal.ui.SubmitHandler +@Suppress("TooManyFunctions") class ACHDirectDebitComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< ACHDirectDebitComponent, ACHDirectDebitConfiguration, ACHDirectDebitComponentState, - ComponentCallback + ComponentCallback, >, StoredPaymentComponentProvider< ACHDirectDebitComponent, ACHDirectDebitConfiguration, ACHDirectDebitComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< ACHDirectDebitComponent, ACHDirectDebitConfiguration, ACHDirectDebitComponentState, - SessionComponentCallback + SessionComponentCallback, >, SessionStoredPaymentComponentProvider< ACHDirectDebitComponent, ACHDirectDebitConfiguration, ACHDirectDebitComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ACHDirectDebitComponentParamsMapper( - overrideComponentParams, - overrideSessionParams - ) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: ACHDirectDebitConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, key: String?, ): ACHDirectDebitComponent { assertSupported(paymentMethod) + val achFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) - val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) - val publicKeyService = PublicKeyService(httpClient) - val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) + val componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) - val addressService = AddressService(httpClient) - val addressRepository = DefaultAddressRepository(addressService) - val genericEncryptor = GenericEncryptorFactory.provide() + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( application = application, @@ -123,39 +128,31 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) - val achDelegate = DefaultACHDirectDebitDelegate( - observerRepository = PaymentObserverRepository(), + val achDelegate = createDefaultDelegate( paymentMethod = paymentMethod, - analyticsRepository = analyticsRepository, - publicKeyRepository = publicKeyRepository, - addressRepository = addressRepository, - submitHandler = SubmitHandler(savedStateHandle), - genericEncryptor = genericEncryptor, + savedStateHandle = savedStateHandle, componentParams = componentParams, - order = order + analyticsRepository = analyticsRepository, + httpClient = httpClient, + order = order, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + createComponent( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, - ) - - ACHDirectDebitComponent( - achDirectDebitDelegate = achDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, achDelegate), - componentEventHandler = DefaultComponentEventHandler() + delegate = achDelegate, + componentEventHandler = DefaultComponentEventHandler(), ) } return ViewModelProvider( viewModelStoreOwner, - achFactory + achFactory, )[key, ACHDirectDebitComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) @@ -163,6 +160,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: ACHDirectDebitConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String?, + ): ACHDirectDebitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -170,24 +191,22 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: ACHDirectDebitConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? ): ACHDirectDebitComponent { assertSupported(paymentMethod) + val achFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), ) - val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) - val publicKeyService = PublicKeyService(httpClient) - val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) - val addressService = AddressService(httpClient) - val addressRepository = DefaultAddressRepository(addressService) - val genericEncryptor = GenericEncryptorFactory.provide() + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( application = application, @@ -196,57 +215,39 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) - val achDelegate = DefaultACHDirectDebitDelegate( - observerRepository = PaymentObserverRepository(), + val achDelegate = createDefaultDelegate( paymentMethod = paymentMethod, - analyticsRepository = analyticsRepository, - publicKeyRepository = publicKeyRepository, - addressRepository = addressRepository, - submitHandler = SubmitHandler(savedStateHandle), - genericEncryptor = genericEncryptor, - componentParams = componentParams, - order = checkoutSession.order - ) - - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, savedStateHandle = savedStateHandle, - application = application, + componentParams = componentParams, + analyticsRepository = analyticsRepository, + httpClient = httpClient, + order = checkoutSession.order, ) - val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + val sessionComponentEventHandler = createSessionComponentEventHandler( savedStateHandle = savedStateHandle, checkoutSession = checkoutSession, - ) - val sessionInteractor = SessionInteractor( - sessionRepository = SessionRepository( - sessionService = SessionService(httpClient), - clientKey = componentParams.clientKey, - ), - sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false - ) - - val sessionComponentEventHandler = SessionComponentEventHandler( - sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, + httpClient = httpClient, + componentParams = componentParams, ) - ACHDirectDebitComponent( - achDirectDebitDelegate = achDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, achDelegate), - componentEventHandler = sessionComponentEventHandler, + createComponent( + checkoutConfiguration, + savedStateHandle, + application, + achDelegate, + sessionComponentEventHandler, ) } + return ViewModelProvider( viewModelStoreOwner, - achFactory + achFactory, )[key, ACHDirectDebitComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) @@ -254,13 +255,65 @@ constructor( } } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, - storedPaymentMethod: StoredPaymentMethod, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, configuration: ACHDirectDebitConfiguration, application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): ACHDirectDebitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + + @Suppress("LongParameterList") + private fun createDefaultDelegate( + paymentMethod: PaymentMethod, + savedStateHandle: SavedStateHandle, + componentParams: ACHDirectDebitComponentParams, + analyticsRepository: AnalyticsRepository, + httpClient: HttpClient, + order: Order?, + ): DefaultACHDirectDebitDelegate { + val publicKeyService = PublicKeyService(httpClient) + val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) + val addressService = AddressService(httpClient) + val addressRepository = DefaultAddressRepository(addressService) + val genericEncryptor = GenericEncryptorFactory.provide() + return DefaultACHDirectDebitDelegate( + observerRepository = PaymentObserverRepository(), + paymentMethod = paymentMethod, + analyticsRepository = analyticsRepository, + publicKeyRepository = publicKeyRepository, + addressRepository = addressRepository, + submitHandler = SubmitHandler(savedStateHandle), + genericEncryptor = genericEncryptor, + componentParams = componentParams, + order = order, + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, componentCallback: ComponentCallback, order: Order?, key: String? @@ -268,7 +321,12 @@ constructor( assertSupported(storedPaymentMethod) val achFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -277,34 +335,30 @@ constructor( storedPaymentMethod = storedPaymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) - val achDelegate = StoredACHDirectDebitDelegate( - observerRepository = PaymentObserverRepository(), - storedPaymentMethod = storedPaymentMethod, - analyticsRepository = analyticsRepository, + val achDelegate = createStoredDelegate( + paymentMethod = storedPaymentMethod, componentParams = componentParams, - order = order + analyticsRepository = analyticsRepository, + order = order, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + createComponent( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, - ) - ACHDirectDebitComponent( - achDirectDebitDelegate = achDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, achDelegate), - componentEventHandler = DefaultComponentEventHandler() + delegate = achDelegate, + componentEventHandler = DefaultComponentEventHandler(), ) } + return ViewModelProvider( viewModelStoreOwner, - achFactory + achFactory, )[key, ACHDirectDebitComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) @@ -312,6 +366,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + configuration: ACHDirectDebitConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): ACHDirectDebitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -319,7 +397,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, storedPaymentMethod: StoredPaymentMethod, - configuration: ACHDirectDebitConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -327,10 +405,13 @@ constructor( assertSupported(storedPaymentMethod) val achFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -341,55 +422,37 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) - val achDelegate = StoredACHDirectDebitDelegate( - observerRepository = PaymentObserverRepository(), - storedPaymentMethod = storedPaymentMethod, + val achDelegate = createStoredDelegate( + paymentMethod = storedPaymentMethod, analyticsRepository = analyticsRepository, componentParams = componentParams, - order = checkoutSession.order + order = checkoutSession.order, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, - savedStateHandle = savedStateHandle, - application = application, - ) - - val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + val sessionComponentEventHandler = createSessionComponentEventHandler( savedStateHandle = savedStateHandle, checkoutSession = checkoutSession, + httpClient = httpClient, + componentParams = componentParams, ) - val sessionInteractor = SessionInteractor( - sessionRepository = SessionRepository( - sessionService = SessionService(httpClient), - clientKey = componentParams.clientKey, - ), - sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false - ) - - val sessionComponentEventHandler = - SessionComponentEventHandler( - sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, - ) - - ACHDirectDebitComponent( - achDirectDebitDelegate = achDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, achDelegate), + createComponent( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + delegate = achDelegate, componentEventHandler = sessionComponentEventHandler, ) } + return ViewModelProvider( viewModelStoreOwner, - achFactory + achFactory, )[key, ACHDirectDebitComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) @@ -397,6 +460,92 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + configuration: ACHDirectDebitConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): ACHDirectDebitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + + private fun createStoredDelegate( + paymentMethod: StoredPaymentMethod, + componentParams: ACHDirectDebitComponentParams, + analyticsRepository: AnalyticsRepository, + order: Order? + ): StoredACHDirectDebitDelegate { + return StoredACHDirectDebitDelegate( + observerRepository = PaymentObserverRepository(), + storedPaymentMethod = paymentMethod, + analyticsRepository = analyticsRepository, + componentParams = componentParams, + order = order, + ) + } + + private fun createComponent( + checkoutConfiguration: CheckoutConfiguration, + savedStateHandle: SavedStateHandle, + application: Application, + delegate: ACHDirectDebitDelegate, + componentEventHandler: ComponentEventHandler, + ): ACHDirectDebitComponent { + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + ) + + return ACHDirectDebitComponent( + achDirectDebitDelegate = delegate, + genericActionDelegate = genericActionDelegate, + actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, delegate), + componentEventHandler = componentEventHandler, + ) + } + + private fun createSessionComponentEventHandler( + savedStateHandle: SavedStateHandle, + checkoutSession: CheckoutSession, + httpClient: HttpClient, + componentParams: ACHDirectDebitComponentParams, + ): SessionComponentEventHandler { + val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + savedStateHandle = savedStateHandle, + checkoutSession = checkoutSession, + ) + + val sessionInteractor = SessionInteractor( + sessionRepository = SessionRepository( + sessionService = SessionService(httpClient), + clientKey = componentParams.clientKey, + ), + sessionModel = sessionSavedStateHandleContainer.getSessionModel(), + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, + ) + + return SessionComponentEventHandler( + sessionInteractor = sessionInteractor, + sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt index f7b0995eb9..b51d517f2a 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt @@ -22,12 +22,13 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.ACHDirectDebitPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.cse.EncryptionException import com.adyen.checkout.cse.internal.BaseGenericEncryptor import com.adyen.checkout.ui.core.internal.data.api.AddressRepository @@ -39,7 +40,6 @@ import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIState import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import com.adyen.checkout.ui.core.internal.ui.UIStateDelegate -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.ui.model.AddressParams @@ -134,7 +134,7 @@ internal class DefaultACHDirectDebitDelegate( private fun onInputDataChanged() { val outputData = createOutputData( countryOptions = outputData.addressState.countryOptions, - stateOptions = outputData.addressState.stateOptions + stateOptions = outputData.addressState.stateOptions, ) _outputDataFlow.tryEmit(outputData) updateComponentState(outputData) @@ -151,11 +151,11 @@ internal class DefaultACHDirectDebitDelegate( ): ACHDirectDebitOutputData { val updatedCountryOptions = AddressFormUtils.markAddressListItemSelected( countryOptions, - inputData.address.country + inputData.address.country, ) val updatedStateOptions = AddressFormUtils.markAddressListItemSelected( stateOptions, - inputData.address.stateOrProvince + inputData.address.stateOrProvince, ) val addressFormUIState = AddressFormUIState.fromAddressParams(componentParams.addressParams) @@ -169,30 +169,30 @@ internal class DefaultACHDirectDebitDelegate( addressFormUIState, updatedCountryOptions, updatedStateOptions, - false + false, ), addressUIState = addressFormUIState, shouldStorePaymentMethod = inputData.isStorePaymentMethodSwitchChecked, - showStorePaymentField = showStorePaymentField() + showStorePaymentField = showStorePaymentField(), ) } private fun fetchPublicKey(coroutineScope: CoroutineScope) { - Logger.d(TAG, "fetchPublicKey") + adyenLog(AdyenLogLevel.DEBUG) { "fetchPublicKey" } coroutineScope.launch { publicKeyRepository.fetchPublicKey( environment = componentParams.environment, - clientKey = componentParams.clientKey + clientKey = componentParams.clientKey, ).fold( onSuccess = { key -> - Logger.d(TAG, "Public key fetched") + adyenLog(AdyenLogLevel.DEBUG) { "Public key fetched" } publicKey = key updateComponentState(outputData) }, onFailure = { e -> - Logger.e(TAG, "Unable to fetch public key") + adyenLog(AdyenLogLevel.ERROR) { "Unable to fetch public key" } exceptionChannel.trySend(ComponentException("Unable to fetch publicKey.", e)) - } + }, ) } } @@ -201,11 +201,11 @@ internal class DefaultACHDirectDebitDelegate( addressRepository.countriesFlow .distinctUntilChanged() .onEach { countries -> - Logger.d(TAG, "New countries emitted - countries: ${countries.size}") + adyenLog(AdyenLogLevel.DEBUG) { "New countries emitted - countries: ${countries.size}" } val countryOptions = AddressFormUtils.initializeCountryOptions( shopperLocale = componentParams.shopperLocale, addressParams = componentParams.addressParams, - countryList = countries + countryList = countries, ) countryOptions.firstOrNull { it.selected }?.let { inputData.address.country = it.code @@ -220,7 +220,7 @@ internal class DefaultACHDirectDebitDelegate( addressRepository.statesFlow .distinctUntilChanged() .onEach { states -> - Logger.d(TAG, "New states emitted - states: ${states.size}") + adyenLog(AdyenLogLevel.DEBUG) { "New states emitted - states: ${states.size}" } updateOutputData(stateOptions = AddressFormUtils.initializeStateOptions(states)) } .launchIn(coroutineScope) @@ -239,26 +239,26 @@ internal class DefaultACHDirectDebitDelegate( addressRepository.getStateList( shopperLocale = componentParams.shopperLocale, countryCode = countryCode, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, ) } private fun requestCountryList() { addressRepository.getCountryList( shopperLocale = componentParams.shopperLocale, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, ) } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } } private fun updateComponentState(outputData: ACHDirectDebitOutputData) { - Logger.v(TAG, "updateComponentState") + adyenLog(AdyenLogLevel.VERBOSE) { "updateComponentState" } val componentState = createComponentState(outputData) _componentStateFlow.tryEmit(componentState) } @@ -272,7 +272,7 @@ internal class DefaultACHDirectDebitDelegate( return ACHDirectDebitComponentState( data = PaymentComponentData(null, null, null), isInputValid = outputData.isValid, - isReady = publicKey != null + isReady = publicKey != null, ) } @@ -280,12 +280,12 @@ internal class DefaultACHDirectDebitDelegate( val encryptedBankAccountNumber = genericEncryptor.encryptField( fieldKeyToEncrypt = ENCRYPTION_KEY_FOR_BANK_ACCOUNT_NUMBER, fieldValueToEncrypt = outputData.bankAccountNumber.value, - publicKey = publicKey + publicKey = publicKey, ) val encryptedBankLocationId = genericEncryptor.encryptField( fieldKeyToEncrypt = ENCRYPTION_KEY_FOR_BANK_LOCATION_ID, fieldValueToEncrypt = outputData.bankLocationId.value, - publicKey = publicKey + publicKey = publicKey, ) val achPaymentMethod = ACHDirectDebitPaymentMethod( @@ -305,7 +305,7 @@ internal class DefaultACHDirectDebitDelegate( if (isAddressRequired(outputData.addressUIState)) { paymentComponentData.billingAddress = AddressFormUtils.makeAddressData( addressOutputData = outputData.addressState, - addressFormUIState = outputData.addressUIState + addressFormUIState = outputData.addressUIState, ) } @@ -315,7 +315,7 @@ internal class DefaultACHDirectDebitDelegate( return ACHDirectDebitComponentState( data = PaymentComponentData(null, null, null), isInputValid = false, - isReady = true + isReady = true, ) } } @@ -339,7 +339,7 @@ internal class DefaultACHDirectDebitDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -359,7 +359,7 @@ internal class DefaultACHDirectDebitDelegate( override fun onSubmit() { val state = _componentStateFlow.value submitHandler.onSubmit( - state = state + state = state, ) } @@ -368,7 +368,6 @@ internal class DefaultACHDirectDebitDelegate( override fun shouldShowSubmitButton(): Boolean = isConfirmationRequired() && componentParams.isSubmitButtonVisible companion object { - private val TAG = LogUtil.getTag() private const val ENCRYPTION_KEY_FOR_BANK_ACCOUNT_NUMBER = "bankAccountNumber" private const val ENCRYPTION_KEY_FOR_BANK_LOCATION_ID = "bankLocationId" } diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegate.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegate.kt index fbb33c6980..41bcbe2ed2 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegate.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegate.kt @@ -20,16 +20,16 @@ import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.ACHDirectDebitPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.ComponentViewType -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.util.AddressValidationUtils import kotlinx.coroutines.CoroutineScope @@ -101,11 +101,11 @@ internal class StoredACHDirectDebitDelegate( } override fun updateInputData(update: ACHDirectDebitInputData.() -> Unit) { - Logger.e(TAG, "updateInputData should not be called in StoredACHDirectDebitDelegate") + adyenLog(AdyenLogLevel.ERROR) { "updateInputData should not be called in StoredACHDirectDebitDelegate" } } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -127,7 +127,7 @@ internal class StoredACHDirectDebitDelegate( return ACHDirectDebitComponentState( data = paymentComponentData, isInputValid = true, - isReady = true + isReady = true, ) } @@ -163,7 +163,7 @@ internal class StoredACHDirectDebitDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -172,10 +172,6 @@ internal class StoredACHDirectDebitDelegate( } override fun updateAddressInputData(update: AddressInputModel.() -> Unit) { - Logger.e(TAG, "updateAddressInputData should not be called in StoredACHDirectDebitDelegate") - } - - companion object { - private val TAG = LogUtil.getTag() + adyenLog(AdyenLogLevel.ERROR) { "updateAddressInputData should not be called in StoredACHDirectDebitDelegate" } } } diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParams.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParams.kt index d433a1c019..200be8e704 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParams.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParams.kt @@ -8,22 +8,14 @@ package com.adyen.checkout.ach.internal.ui.model -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams -import java.util.Locale internal data class ACHDirectDebitComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, val addressParams: AddressParams, val isStorePaymentFieldVisible: Boolean, -) : ComponentParams, ButtonParams +) : ComponentParams by commonComponentParams, ButtonParams diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapper.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapper.kt index 2c04314936..105454cd6a 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapper.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapper.kt @@ -10,41 +10,54 @@ package com.adyen.checkout.ach.internal.ui.model import com.adyen.checkout.ach.ACHDirectDebitAddressConfiguration import com.adyen.checkout.ach.ACHDirectDebitConfiguration -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.ach.getACHDirectDebitConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.ui.core.internal.ui.model.AddressParams +import java.util.Locale internal class ACHDirectDebitComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - configuration: ACHDirectDebitConfiguration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, ): ACHDirectDebitComponentParams { - return configuration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + val achDirectDebitConfiguration = checkoutConfiguration.getACHDirectDebitConfiguration() + return mapToParams( + commonComponentParamsMapperData.commonComponentParams, + commonComponentParamsMapperData.sessionParams, + achDirectDebitConfiguration, + ) } - private fun ACHDirectDebitConfiguration.mapToParamsInternal(): ACHDirectDebitComponentParams { + private fun mapToParams( + commonComponentParams: CommonComponentParams, + sessionParams: SessionParams?, + achDirectDebitConfiguration: ACHDirectDebitConfiguration?, + ): ACHDirectDebitComponentParams { return ACHDirectDebitComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - isSubmitButtonVisible = isSubmitButtonVisible ?: true, - addressParams = addressConfiguration?.mapToAddressParam() + commonComponentParams = commonComponentParams, + isSubmitButtonVisible = achDirectDebitConfiguration?.isSubmitButtonVisible ?: true, + addressParams = achDirectDebitConfiguration?.addressConfiguration?.mapToAddressParam() ?: AddressParams.FullAddress( supportedCountryCodes = DEFAULT_SUPPORTED_COUNTRY_LIST, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ), - isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: true, + isStorePaymentFieldVisible = sessionParams?.enableStoreDetails + ?: achDirectDebitConfiguration?.isStorePaymentFieldVisible ?: true, ) } @@ -53,39 +66,16 @@ internal class ACHDirectDebitComponentParamsMapper( is ACHDirectDebitAddressConfiguration.None -> { AddressParams.None } + is ACHDirectDebitAddressConfiguration.FullAddress -> { AddressParams.FullAddress( supportedCountryCodes = supportedCountryCodes, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ) } } } - private fun ACHDirectDebitComponentParams.override( - overrideComponentParams: ComponentParams? - ): ACHDirectDebitComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) - } - - private fun ACHDirectDebitComponentParams.override( - sessionParams: SessionParams? = null - ): ACHDirectDebitComponentParams { - if (sessionParams == null) return this - return copy( - isStorePaymentFieldVisible = sessionParams.enableStoreDetails ?: isStorePaymentFieldVisible, - amount = sessionParams.amount ?: amount, - ) - } - companion object { private val DEFAULT_SUPPORTED_COUNTRY_LIST = listOf("US", "PR") } diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitInputData.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitInputData.kt index af0702df85..a9e774e1a4 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitInputData.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitInputData.kt @@ -8,8 +8,8 @@ package com.adyen.checkout.ach.internal.ui.model +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.InputData -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel internal data class ACHDirectDebitInputData( var bankAccountNumber: String = "", diff --git a/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitConfigurationTest.kt b/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitConfigurationTest.kt new file mode 100644 index 0000000000..3f53026a2a --- /dev/null +++ b/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitConfigurationTest.kt @@ -0,0 +1,98 @@ +package com.adyen.checkout.ach + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class ACHDirectDebitConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + achDirectDebit { + setSubmitButtonVisible(false) + setAddressConfiguration(ACHDirectDebitAddressConfiguration.FullAddress(listOf("test"))) + setShowStorePaymentField(true) + } + } + + val actual = checkoutConfiguration.getACHDirectDebitConfiguration() + + val expected = ACHDirectDebitConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .setAddressConfiguration(ACHDirectDebitAddressConfiguration.FullAddress(listOf("test"))) + .setShowStorePaymentField(true) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + assertEquals(expected.addressConfiguration, actual?.addressConfiguration) + assertEquals(expected.isStorePaymentFieldVisible, actual?.isStorePaymentFieldVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = ACHDirectDebitConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .setAddressConfiguration(ACHDirectDebitAddressConfiguration.FullAddress(listOf("test"))) + .setShowStorePaymentField(true) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualACHConfig = actual.getACHDirectDebitConfiguration() + assertEquals(config.shopperLocale, actualACHConfig?.shopperLocale) + assertEquals(config.environment, actualACHConfig?.environment) + assertEquals(config.clientKey, actualACHConfig?.clientKey) + assertEquals(config.amount, actualACHConfig?.amount) + assertEquals(config.analyticsConfiguration, actualACHConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualACHConfig?.isSubmitButtonVisible) + assertEquals(config.addressConfiguration, actualACHConfig?.addressConfiguration) + assertEquals(config.isStorePaymentFieldVisible, actualACHConfig?.isStorePaymentFieldVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt index 172965f178..3bcc431788 100644 --- a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt +++ b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt @@ -13,8 +13,10 @@ import com.adyen.checkout.ach.ACHDirectDebitAddressConfiguration import com.adyen.checkout.ach.ACHDirectDebitComponentState import com.adyen.checkout.ach.ACHDirectDebitConfiguration import com.adyen.checkout.ach.R +import com.adyen.checkout.ach.achDirectDebit import com.adyen.checkout.ach.internal.ui.model.ACHDirectDebitComponentParamsMapper import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentMethod @@ -22,6 +24,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.paymentmethod.ACHDirectDebitPaymentMethod @@ -34,7 +38,6 @@ import com.adyen.checkout.ui.core.internal.data.api.AddressRepository import com.adyen.checkout.ui.core.internal.test.TestAddressRepository import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.util.AddressFormUtils @@ -111,9 +114,9 @@ internal class DefaultACHDirectDebitDelegateTest( fun `when isStorePaymentFieldVisible in configuration is false, isStorePaymentFieldVisible in outputdata should be false`() = runTest { delegate = createAchDelegate( - configuration = getAchConfigurationBuilder() - .setShowStorePaymentField(false) - .build() + configuration = createCheckoutConfiguration { + setShowStorePaymentField(false) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -128,9 +131,9 @@ internal class DefaultACHDirectDebitDelegateTest( fun `when isStorePaymentFieldVisible in configuration is true , isStorePaymentFieldVisible in outputdata should be true`() = runTest { delegate = createAchDelegate( - configuration = getAchConfigurationBuilder() - .setShowStorePaymentField(true) - .build() + configuration = createCheckoutConfiguration { + setShowStorePaymentField(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -144,9 +147,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `address configuration is none, then countries and states should not be fetched`() = runTest { delegate = createAchDelegate( - configuration = getAchConfigurationBuilder() - .setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) - .build() + configuration = createCheckoutConfiguration { + setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) + }, ) val countriesTestFlow = addressRepository.countriesFlow.test(testScheduler) @@ -179,8 +182,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when address configuration is NONE, addressUIState in outputdata must be NONE`() = runTest { - val configuration = - getAchConfigurationBuilder().setAddressConfiguration(ACHDirectDebitAddressConfiguration.None).build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) + } delegate = createAchDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -191,12 +195,13 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when address configuration is FullAddress, addressUIState in outputdata must be FullAddress`() = runTest { - val configuration = - getAchConfigurationBuilder().setAddressConfiguration( + val configuration = createCheckoutConfiguration { + setAddressConfiguration( ACHDirectDebitAddressConfiguration.FullAddress( - supportedCountryCodes = DEFAULT_SUPPORTED_COUNTRY_LIST - ) - ).build() + supportedCountryCodes = DEFAULT_SUPPORTED_COUNTRY_LIST, + ), + ) + } delegate = createAchDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -207,17 +212,21 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when the address is changed, addressOutputDataFlow should be notified with the same data`() = runTest { - val configuration = - getAchConfigurationBuilder().setAddressConfiguration( - ACHDirectDebitAddressConfiguration.FullAddress( - DEFAULT_SUPPORTED_COUNTRY_LIST - ) - ).build() - val componentParams = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(configuration, null) + val configuration = createCheckoutConfiguration { + setAddressConfiguration( + ACHDirectDebitAddressConfiguration.FullAddress(DEFAULT_SUPPORTED_COUNTRY_LIST), + ) + } + val componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + ) val countryOptions = AddressFormUtils.initializeCountryOptions( shopperLocale = componentParams.shopperLocale, addressParams = componentParams.addressParams, - countryList = TestAddressRepository.COUNTRIES + countryList = TestAddressRepository.COUNTRIES, ) val expectedCountries = AddressFormUtils.markAddressListItemSelected( @@ -232,7 +241,7 @@ internal class DefaultACHDirectDebitDelegateTest( houseNumberOrName = "44", apartmentSuite = "aparment", city = "Istanbul", - country = "Turkey" + country = "Turkey", ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -251,7 +260,7 @@ internal class DefaultACHDirectDebitDelegateTest( assertEquals(expectedCountries, countryOptions) assertEquals( stateOptions, - AddressFormUtils.initializeStateOptions(TestAddressRepository.STATES) + AddressFormUtils.initializeStateOptions(TestAddressRepository.STATES), ) } } @@ -271,7 +280,7 @@ internal class DefaultACHDirectDebitDelegateTest( val outputData = delegate.outputDataFlow.first() assertEquals( Validation.Invalid(reason = R.string.checkout_ach_bank_account_number_invalid), - outputData.bankAccountNumber.validation + outputData.bankAccountNumber.validation, ) } @@ -290,7 +299,7 @@ internal class DefaultACHDirectDebitDelegateTest( val outputData = delegate.outputDataFlow.first() assertEquals( Validation.Invalid(reason = R.string.checkout_ach_bank_account_location_invalid), - outputData.bankLocationId.validation + outputData.bankLocationId.validation, ) } @@ -309,7 +318,7 @@ internal class DefaultACHDirectDebitDelegateTest( val outputData = delegate.outputDataFlow.first() assertEquals( Validation.Invalid(reason = R.string.checkout_ach_bank_account_holder_name_invalid), - outputData.ownerName.validation + outputData.ownerName.validation, ) } } @@ -340,7 +349,7 @@ internal class DefaultACHDirectDebitDelegateTest( houseNumberOrName = "44", apartmentSuite = "aparment", city = "Istanbul", - country = "Turkey" + country = "Turkey", ) } @@ -351,8 +360,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when bankLocationId is invalid, then component state should be invalid`() = runTest { - val configuration = - getAchConfigurationBuilder().setAddressConfiguration(ACHDirectDebitAddressConfiguration.None).build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) + } delegate = createAchDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -372,8 +382,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when bankAccountNumber is invalid, then component state should be invalid`() = runTest { - val configuration = - getAchConfigurationBuilder().setAddressConfiguration(ACHDirectDebitAddressConfiguration.None).build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) + } delegate = createAchDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -393,8 +404,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when ownerName is invalid, then component state should be invalid`() = runTest { - val configuration = - getAchConfigurationBuilder().setAddressConfiguration(ACHDirectDebitAddressConfiguration.None).build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) + } delegate = createAchDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -432,9 +444,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when all fields in outputdata are valid and address is empty and not required, then component state should be valid`() = runTest { - val configuration = getAchConfigurationBuilder() - .setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) - .build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(ACHDirectDebitAddressConfiguration.None) + } delegate = createAchDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -464,7 +476,7 @@ internal class DefaultACHDirectDebitDelegateTest( apartmentSuite = FieldState(adddressInputModel.apartmentSuite, Validation.Valid), city = FieldState(adddressInputModel.city, Validation.Valid), country = FieldState(adddressInputModel.country, Validation.Valid), - isOptional = false + isOptional = false, ) val addressUIState = AddressFormUIState.FULL_ADDRESS @@ -507,16 +519,16 @@ internal class DefaultACHDirectDebitDelegateTest( @ParameterizedTest @MethodSource( - "com.adyen.checkout.ach.internal.ui.DefaultACHDirectDebitDelegateTest#shouldStorePaymentMethodSource" + "com.adyen.checkout.ach.internal.ui.DefaultACHDirectDebitDelegateTest#shouldStorePaymentMethodSource", ) fun `storePaymentMethod in component state should match store switch visibility and state`( isStorePaymentMethodSwitchVisible: Boolean, isStorePaymentMethodSwitchChecked: Boolean, expectedStorePaymentMethod: Boolean?, ) = runTest { - val configuration = getAchConfigurationBuilder() - .setShowStorePaymentField(isStorePaymentMethodSwitchVisible) - .build() + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(isStorePaymentMethodSwitchVisible) + } delegate = createAchDelegate(configuration = configuration) val adddressInputModel = AddressInputModel( @@ -526,7 +538,7 @@ internal class DefaultACHDirectDebitDelegateTest( houseNumberOrName = "44", apartmentSuite = "aparment", city = "Istanbul", - country = "Turkey" + country = "Turkey", ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -551,9 +563,7 @@ internal class DefaultACHDirectDebitDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getAchConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createAchDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -574,9 +584,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createAchDelegate( - configuration = getAchConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -585,9 +595,9 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createAchDelegate( - configuration = getAchConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -666,7 +676,7 @@ internal class DefaultACHDirectDebitDelegateTest( country = country, isOptional = isOptional, countryOptions = countryOptions, - stateOptions = stateOptions + stateOptions = stateOptions, ) } @@ -678,7 +688,7 @@ internal class DefaultACHDirectDebitDelegateTest( addressRepository: AddressRepository = this.addressRepository, genericEncryptor: BaseGenericEncryptor = this.genericEncryptor, submitHandler: SubmitHandler = this.submitHandler, - configuration: ACHDirectDebitConfiguration = getAchConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: OrderRequest? = TEST_ORDER, ) = DefaultACHDirectDebitDelegate( observerRepository = PaymentObserverRepository(), @@ -688,15 +698,22 @@ internal class DefaultACHDirectDebitDelegateTest( addressRepository = addressRepository, submitHandler = submitHandler, genericEncryptor = genericEncryptor, - componentParams = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(configuration, null), - order = order + componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, DEVICE_LOCALE, null, null), + order = order, ) - private fun getAchConfigurationBuilder() = ACHDirectDebitConfiguration.Builder( + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: ACHDirectDebitConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( shopperLocale = Locale.US, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY, - ) + amount = amount, + ) { + achDirectDebit(configuration) + } private fun getValidAddressInputData(): AddressInputModel { return AddressInputModel( @@ -704,9 +721,9 @@ internal class DefaultACHDirectDebitDelegateTest( street = "Street Name", stateOrProvince = "province", houseNumberOrName = "44", - apartmentSuite = "aparment", + apartmentSuite = "apartment", city = "Istanbul", - country = "Turkey" + country = "Turkey", ) } @@ -718,6 +735,7 @@ internal class DefaultACHDirectDebitDelegateTest( private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") private val DEFAULT_SUPPORTED_COUNTRY_LIST = listOf("US", "PR") private const val TEST_CHECKOUT_ATTEMPT_ID = "TEST_CHECKOUT_ATTEMPT_ID" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun shouldStorePaymentMethodSource() = listOf( diff --git a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegateTest.kt b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegateTest.kt index 1fb8a405c9..27eea5d09a 100644 --- a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegateTest.kt +++ b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/StoredACHDirectDebitDelegateTest.kt @@ -10,12 +10,15 @@ package com.adyen.checkout.ach.internal.ui import app.cash.turbine.test import com.adyen.checkout.ach.ACHDirectDebitConfiguration +import com.adyen.checkout.ach.achDirectDebit import com.adyen.checkout.ach.internal.ui.model.ACHDirectDebitComponentParamsMapper import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment import com.adyen.checkout.test.TestDispatcherExtension import kotlinx.coroutines.CoroutineScope @@ -67,9 +70,7 @@ internal class StoredACHDirectDebitDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getAchConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createAchDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -103,30 +104,38 @@ internal class StoredACHDirectDebitDelegateTest( } } - private fun getAchConfigurationBuilder() = ACHDirectDebitConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY, - ) - private fun createAchDelegate( paymentMethod: StoredPaymentMethod = StoredPaymentMethod(id = STORED_ID), analyticsRepository: AnalyticsRepository = this.analyticsRepository, - configuration: ACHDirectDebitConfiguration = getAchConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: OrderRequest? = TEST_ORDER, ) = StoredACHDirectDebitDelegate( observerRepository = PaymentObserverRepository(), storedPaymentMethod = paymentMethod, analyticsRepository = analyticsRepository, - componentParams = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(configuration, null), - order = order + componentParams = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, DEVICE_LOCALE, null, null), + order = order, ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: ACHDirectDebitConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + achDirectDebit(configuration) + } + companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") private const val STORED_ID = "Stored_id" private const val TEST_CHECKOUT_ATTEMPT_ID = "TEST_CHECKOUT_ATTEMPT_ID" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun amountSource() = listOf( diff --git a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt index c8889045fb..ee28dba073 100644 --- a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt +++ b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/model/ACHDirectDebitComponentParamsMapperTest.kt @@ -10,10 +10,17 @@ package com.adyen.checkout.ach.internal.ui.model import com.adyen.checkout.ach.ACHDirectDebitAddressConfiguration import com.adyen.checkout.ach.ACHDirectDebitConfiguration +import com.adyen.checkout.ach.achDirectDebit import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams @@ -26,11 +33,13 @@ import java.util.Locale internal class ACHDirectDebitComponentParamsMapperTest { + private val achDirectDebitComponentParamsMapper = ACHDirectDebitComponentParamsMapper(CommonComponentParamsMapper()) + @Test - fun `when parent configuration is null and custom ach configuration fields are null then all fields should match`() { - val achConfiguration = getAchConfigurationBuilder().build() + fun `when drop-in override params are null and custom ach configuration fields are null then all fields should match`() { + val configuration = createCheckoutConfiguration() - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val params = achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams() @@ -38,35 +47,43 @@ internal class ACHDirectDebitComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom ach configuration fields are set then all fields should match`() { + fun `when drop-in override params are null and custom ach configuration fields are set then all fields should match`() { val addressConfiguration = ACHDirectDebitAddressConfiguration.FullAddress(supportedCountryCodes = SUPPORTED_COUNTRY_LIST) - val achConfiguration = getAchConfigurationBuilder() - .setAddressConfiguration(addressConfiguration) - .build() - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val configuration = createCheckoutConfiguration { + setAddressConfiguration(addressConfiguration) + } + val params = achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams() assertEquals(expected, params) } @Test - fun `when parent configuration is set then parent configuration fields should override ach configuration fields`() { - val achConfiguration = getAchConfigurationBuilder().build() - - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override ach configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "CAD", - value = 1235_00L - ) + value = 1235_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + achDirectDebit { + setAmount(Amount("USD", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } + + val dropInOverrideParams = DropInOverrideParams(Amount("EUR", 123L), null) + val params = achDirectDebitComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, ) - val params = ACHDirectDebitComponentParamsMapper(overrideParams, null).mapToParams(achConfiguration, null) - val expected = getAchComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, @@ -74,9 +91,9 @@ internal class ACHDirectDebitComponentParamsMapperTest { analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "CAD", - value = 1235_00L - ) + currency = "EUR", + value = 123L, + ), ) assertEquals(expected, params) @@ -86,11 +103,11 @@ internal class ACHDirectDebitComponentParamsMapperTest { fun `when a address is selected as FullAddress, addressParams should return FullAddress`() { val addressConfiguration = ACHDirectDebitAddressConfiguration.FullAddress(supportedCountryCodes = SUPPORTED_COUNTRY_LIST) - val achConfiguration = getAchConfigurationBuilder() - .setAddressConfiguration(addressConfiguration) - .build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(addressConfiguration) + } - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val params = achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams() assertEquals(expected, params) @@ -99,11 +116,12 @@ internal class ACHDirectDebitComponentParamsMapperTest { @Test fun `when a address is selected as None, addressParams should return None`() { val addressConfiguration = ACHDirectDebitAddressConfiguration.None - val achConfiguration = getAchConfigurationBuilder() - .setAddressConfiguration(addressConfiguration) - .build() + val configuration = createCheckoutConfiguration { + setAddressConfiguration(addressConfiguration) + } - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val params = + achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams(addressParams = AddressParams.None) assertEquals(expected, params) @@ -111,13 +129,14 @@ internal class ACHDirectDebitComponentParamsMapperTest { @Test fun `when the address configuration is null, default address configuration should be FullAddress with default supported countries`() { - val achConfiguration = getAchConfigurationBuilder().build() + val configuration = createCheckoutConfiguration() - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val params = + achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expectedAddressParams: AddressParams = AddressParams.FullAddress( supportedCountryCodes = SUPPORTED_COUNTRY_LIST, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ) assertEquals(expectedAddressParams, params.addressParams) @@ -125,11 +144,12 @@ internal class ACHDirectDebitComponentParamsMapperTest { @Test fun `when the isStorePaymentFieldVisible in configuration is false, isStorePaymentFieldVisible in component params should be false`() { - val achConfiguration = getAchConfigurationBuilder() - .setShowStorePaymentField(false) - .build() + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(false) + } - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val params = + achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams(isStorePaymentFieldVisible = false) @@ -138,11 +158,12 @@ internal class ACHDirectDebitComponentParamsMapperTest { @Test fun `when the isStorePaymentFieldVisible in configuration is true, isStorePaymentFieldVisible in component params should be true`() { - val achConfiguration = getAchConfigurationBuilder() - .setShowStorePaymentField(true) - .build() + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(true) + } - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(achConfiguration, null) + val params = + achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams(isStorePaymentFieldVisible = true) @@ -157,21 +178,15 @@ internal class ACHDirectDebitComponentParamsMapperTest { sessionsValue: Boolean?, expectedValue: Boolean ) { - val achConfiguration = getAchConfigurationBuilder() - .setShowStorePaymentField(configurationValue) - .build() + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(configurationValue) + } - val sessionParams = SessionParams( + val sessionParams = createSessionParams( enableStoreDetails = sessionsValue, - installmentConfiguration = null, - amount = null, - returnUrl = "", ) - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams( - configuration = achConfiguration, - sessionParams = sessionParams - ) + val params = achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, sessionParams) val expected = getAchComponentParams(isStorePaymentFieldVisible = expectedValue) @@ -180,12 +195,9 @@ internal class ACHDirectDebitComponentParamsMapperTest { @Test fun `when isStorePaymentFieldVisible is not set, isStorePaymentFieldVisible should be true`() { - val achConfiguration = getAchConfigurationBuilder().build() + val configuration = createCheckoutConfiguration() - val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams( - configuration = achConfiguration, - sessionParams = null - ) + val params = achDirectDebitComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getAchComponentParams(isStorePaymentFieldVisible = true) @@ -194,46 +206,124 @@ internal class ACHDirectDebitComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val achConfiguration = getAchConfigurationBuilder() - .setAmount(configurationValue) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getAchComponentParams(amount = it) } - - val params = ACHDirectDebitComponentParamsMapper(overrideParams, null).mapToParams( - achConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ) + val configuration = createCheckoutConfiguration(configurationValue) + + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + + val params = achDirectDebitComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, ) val expected = getAchComponentParams( - amount = expectedValue + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, ) assertEquals(expected, params) } - private fun getAchConfigurationBuilder() = ACHDirectDebitConfiguration.Builder( - shopperLocale = Locale.US, + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = achDirectDebitComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getAchComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = achDirectDebitComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getAchComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: ACHDirectDebitConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + achDirectDebit(configuration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, ) @Suppress("LongParameterList") private fun getAchComponentParams( - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -242,25 +332,28 @@ internal class ACHDirectDebitComponentParamsMapperTest { isSubmitButtonVisible: Boolean = true, addressParams: AddressParams = AddressParams.FullAddress( supportedCountryCodes = SUPPORTED_COUNTRY_LIST, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ), isStorePaymentFieldVisible: Boolean = true ) = ACHDirectDebitComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, - amount = amount, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), isSubmitButtonVisible = isSubmitButtonVisible, addressParams = addressParams, - isStorePaymentFieldVisible = isStorePaymentFieldVisible + isStorePaymentFieldVisible = isStorePaymentFieldVisible, ) companion object { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" private val SUPPORTED_COUNTRY_LIST = listOf("US", "PR") + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun enableStoreDetailsSource() = listOf( @@ -280,5 +373,14 @@ internal class ACHDirectDebitComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/action-core/build.gradle b/action-core/build.gradle index eff8347065..a5368082be 100644 --- a/action-core/build.gradle +++ b/action-core/build.gradle @@ -31,6 +31,10 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' consumerProguardFiles "consumer-rules.pro" } + + testOptions { + unitTests.returnDefaultValues = true + } } dependencies { diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt index 232b752ab7..c6adc675c6 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt @@ -7,20 +7,23 @@ */ package com.adyen.checkout.action.core +import android.app.Activity +import android.content.Intent import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.adyen.checkout.action.core.internal.ActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.ActionComponent import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler import com.adyen.checkout.components.core.internal.IntentHandlingComponent import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider import com.adyen.checkout.components.core.internal.ui.ActionDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent import kotlinx.coroutines.flow.Flow @@ -30,13 +33,12 @@ import kotlinx.coroutines.flow.Flow */ class GenericActionComponent internal constructor( private val genericActionDelegate: GenericActionDelegate, - private val actionHandlingComponent: ActionHandlingComponent, internal val actionComponentEventHandler: ActionComponentEventHandler, ) : ViewModel(), ActionComponent, ViewableComponent, IntentHandlingComponent, - ActionHandlingComponent by actionHandlingComponent { + ActionHandlingComponent { override val delegate: ActionDelegate get() = genericActionDelegate.delegate @@ -56,20 +58,35 @@ class GenericActionComponent internal constructor( genericActionDelegate.removeObserver() } + override fun canHandleAction(action: Action): Boolean { + return PROVIDER.canHandleAction(action) + } + + override fun handleAction(action: Action, activity: Activity) { + genericActionDelegate.handleAction(action, activity) + } + + override fun handleIntent(intent: Intent) { + genericActionDelegate.handleIntent(intent) + } + + override fun setOnRedirectListener(listener: () -> Unit) { + genericActionDelegate.setOnRedirectListener(listener) + } + override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } genericActionDelegate.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER: ActionComponentProvider< GenericActionComponent, GenericActionConfiguration, - GenericActionDelegate + GenericActionDelegate, > = GenericActionComponentProvider() } } diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt index 069a8a87c8..a8b6792ba3 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt @@ -16,6 +16,7 @@ import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration import com.adyen.checkout.await.AwaitConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ActionComponent import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration @@ -37,7 +38,7 @@ import kotlin.collections.set */ @Parcelize class GenericActionConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -45,13 +46,8 @@ class GenericActionConfiguration private constructor( private val availableActionConfigs: HashMap, Configuration>, ) : Configuration { - internal inline fun getConfigurationForAction(): T? { - val actionClass = T::class.java - if (availableActionConfigs.containsKey(actionClass)) { - return availableActionConfigs[actionClass] as T - } - return null - } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getAllConfigurations(): List = availableActionConfigs.values.toList() /** * Builder for creating a [GenericActionConfiguration] where you can set specific Configurations for each action @@ -65,6 +61,22 @@ class GenericActionConfiguration private constructor( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val availableActionConfigs = HashMap, Configuration>() + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -72,14 +84,15 @@ class GenericActionConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -88,7 +101,7 @@ class GenericActionConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -151,3 +164,17 @@ class GenericActionConfiguration private constructor( } } } + +internal fun GenericActionConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + this@toCheckoutConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt index c80da43eca..411c6f740f 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt @@ -15,7 +15,6 @@ import com.adyen.checkout.await.AwaitConfiguration import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.LocaleUtil import com.adyen.checkout.qrcode.QRCodeConfiguration import com.adyen.checkout.redirect.RedirectConfiguration import com.adyen.checkout.voucher.VoucherConfiguration @@ -32,21 +31,44 @@ abstract class ActionHandlingPaymentMethodConfigurationBuilder< BuilderT : BaseConfigurationBuilder > /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ constructor( - shopperLocale: Locale, + shopperLocale: Locale?, environment: Environment, clientKey: String ) : BaseConfigurationBuilder(shopperLocale, environment, clientKey), ActionHandlingConfigurationBuilder { protected val genericActionConfigurationBuilder = GenericActionConfiguration.Builder( - shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + ).apply { + shopperLocale?.let { + setShopperLocale(it) + } + } + + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor( + environment: Environment, + clientKey: String + ) : this( + shopperLocale = null, environment = environment, clientKey = clientKey, ) @@ -58,14 +80,16 @@ constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor( + @Suppress("unused") context: Context, environment: Environment, clientKey: String ) : this( - LocaleUtil.getLocale(context), + null, environment, - clientKey + clientKey, ) /** diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt index 1247f36c8b..b830a765d0 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt @@ -20,22 +20,25 @@ import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class DefaultActionHandlingComponent( private val genericActionDelegate: GenericActionDelegate, - paymentDelegate: PaymentComponentDelegate<*>?, + private val paymentDelegate: PaymentComponentDelegate<*>, ) : ActionHandlingComponent { - var activeDelegate: ComponentDelegate = paymentDelegate ?: genericActionDelegate - private set + private var isHandlingAction: Boolean = false + + val activeDelegate: ComponentDelegate + get() = if (isHandlingAction) { + genericActionDelegate.delegate + } else { + paymentDelegate + } override fun canHandleAction(action: Action): Boolean { return GenericActionComponent.PROVIDER.canHandleAction(action) } override fun handleAction(action: Action, activity: Activity) { - activeDelegate = genericActionDelegate + isHandlingAction = true genericActionDelegate.handleAction(action, activity) - // genericActionDelegate.delegate is set when calling genericActionDelegate.handleAction, so we set the more - // specific delegate here as soon as we can. - activeDelegate = genericActionDelegate.delegate } override fun handleIntent(intent: Intent) { diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt index 6615b310a2..9dfc7f7ef2 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt @@ -17,11 +17,12 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.GenericActionComponent import com.adyen.checkout.action.core.GenericActionConfiguration -import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.ActionDelegateProvider import com.adyen.checkout.action.core.internal.ui.DefaultGenericActionDelegate import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.action.core.toCheckoutConfiguration import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.AwaitAction import com.adyen.checkout.components.core.action.QrCodeAction @@ -34,34 +35,34 @@ import com.adyen.checkout.components.core.action.VoucherAction import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.DefaultActionComponentEventHandler import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory +import com.adyen.checkout.core.internal.util.LocaleProvider class GenericActionComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : ActionComponentProvider { - private val componentParamsMapper = GenericComponentParamsMapper(overrideComponentParams, null) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, application: Application, - configuration: GenericActionConfiguration, + checkoutConfiguration: CheckoutConfiguration, callback: ActionComponentCallback, - key: String?, + key: String? ): GenericActionComponent { val genericActionFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val genericActionDelegate = getDelegate(configuration, savedStateHandle, application) + val genericActionDelegate = getDelegate(checkoutConfiguration, savedStateHandle, application) GenericActionComponent( genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, null), - actionComponentEventHandler = DefaultActionComponentEventHandler(callback) + actionComponentEventHandler = DefaultActionComponentEventHandler(callback), ) } return ViewModelProvider(viewModelStoreOwner, genericActionFactory)[key, GenericActionComponent::class.java] @@ -71,17 +72,43 @@ constructor( } override fun getDelegate( - configuration: GenericActionConfiguration, + checkoutConfiguration: CheckoutConfiguration, savedStateHandle: SavedStateHandle, application: Application ): GenericActionDelegate { - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + return DefaultGenericActionDelegate( observerRepository = ActionObserverRepository(), savedStateHandle = savedStateHandle, - configuration = configuration, + checkoutConfiguration = checkoutConfiguration, componentParams = componentParams, - actionDelegateProvider = ActionDelegateProvider(componentParams, null) + actionDelegateProvider = ActionDelegateProvider(dropInOverrideParams), + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + configuration: GenericActionConfiguration, + callback: ActionComponentCallback, + key: String?, + ): GenericActionComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + application = application, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + key = key, ) } diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt index 4da2b00642..951b17dccf 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt @@ -10,11 +10,9 @@ package com.adyen.checkout.action.core.internal.ui import android.app.Application import androidx.lifecycle.SavedStateHandle -import com.adyen.checkout.action.core.GenericActionConfiguration -import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration import com.adyen.checkout.adyen3ds2.internal.provider.Adyen3DS2ComponentProvider -import com.adyen.checkout.await.AwaitConfiguration import com.adyen.checkout.await.internal.provider.AwaitComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.AwaitAction import com.adyen.checkout.components.core.action.BaseThreeds2Action @@ -22,139 +20,40 @@ import com.adyen.checkout.components.core.action.QrCodeAction import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.action.SdkAction import com.adyen.checkout.components.core.action.VoucherAction -import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder -import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.components.core.internal.ui.ActionDelegate -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.runCompileOnly -import com.adyen.checkout.qrcode.QRCodeConfiguration +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.qrcode.internal.provider.QRCodeComponentProvider -import com.adyen.checkout.redirect.RedirectConfiguration import com.adyen.checkout.redirect.internal.provider.RedirectComponentProvider -import com.adyen.checkout.voucher.VoucherConfiguration import com.adyen.checkout.voucher.internal.provider.VoucherComponentProvider -import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration import com.adyen.checkout.wechatpay.internal.provider.WeChatPayActionComponentProvider internal class ActionDelegateProvider( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val dropInOverrideParams: DropInOverrideParams?, + private val localeProvider: LocaleProvider = LocaleProvider(), ) { fun getDelegate( action: Action, - configuration: GenericActionConfiguration, + checkoutConfiguration: CheckoutConfiguration, savedStateHandle: SavedStateHandle, application: Application, ): ActionDelegate { - return when (action) { - is AwaitAction -> { - AwaitComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate( - getConfigurationForAction(configuration), - savedStateHandle, - application - ) - } - - is QrCodeAction -> { - QRCodeComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate( - getConfigurationForAction(configuration), - savedStateHandle, - application - ) - } - - is RedirectAction -> { - RedirectComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate( - getConfigurationForAction(configuration), - savedStateHandle, - application - ) - } - - is BaseThreeds2Action -> { - Adyen3DS2ComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate( - getConfigurationForAction(configuration), - savedStateHandle, - application - ) - } - - is VoucherAction -> { - VoucherComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate( - getConfigurationForAction(configuration), - savedStateHandle, - application - ) - } - - is SdkAction<*> -> { - WeChatPayActionComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate( - getConfigurationForAction(configuration), - savedStateHandle, - application - ) - } - + val provider = when (action) { + is AwaitAction -> AwaitComponentProvider(dropInOverrideParams, localeProvider) + is QrCodeAction -> QRCodeComponentProvider(dropInOverrideParams, localeProvider) + is RedirectAction -> RedirectComponentProvider(dropInOverrideParams, localeProvider) + is BaseThreeds2Action -> Adyen3DS2ComponentProvider(dropInOverrideParams, localeProvider) + is VoucherAction -> VoucherComponentProvider(dropInOverrideParams, localeProvider) + is SdkAction<*> -> WeChatPayActionComponentProvider(dropInOverrideParams, localeProvider) else -> throw CheckoutException("Can't find delegate for action: ${action.type}") } - } - - private inline fun getConfigurationForAction( - configuration: GenericActionConfiguration - ): T { - return configuration.getConfigurationForAction() ?: getDefaultConfiguration(configuration) - } - - private inline fun getDefaultConfiguration( - configuration: Configuration - ): T { - val shopperLocale = configuration.shopperLocale - val environment = configuration.environment - val clientKey = configuration.clientKey - - val builder: BaseConfigurationBuilder<*, *> = when (T::class) { - runCompileOnly { AwaitConfiguration::class } -> AwaitConfiguration.Builder( - shopperLocale, - environment, - clientKey - ) - - runCompileOnly { RedirectConfiguration::class } -> RedirectConfiguration.Builder( - shopperLocale, - environment, - clientKey - ) - - runCompileOnly { QRCodeConfiguration::class } -> QRCodeConfiguration.Builder( - shopperLocale, - environment, - clientKey - ) - - runCompileOnly { Adyen3DS2Configuration::class } -> Adyen3DS2Configuration.Builder( - shopperLocale, - environment, - clientKey - ) - - runCompileOnly { WeChatPayActionConfiguration::class } -> WeChatPayActionConfiguration.Builder( - shopperLocale, - environment, - clientKey - ) - - runCompileOnly { VoucherConfiguration::class } -> VoucherConfiguration.Builder( - shopperLocale, - environment, - clientKey - ) - - else -> throw CheckoutException("Unable to find component configuration for class - ${T::class}") - } - return builder.build() as T + return provider.getDelegate( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + ) } } diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt index 992bd2a8b4..5a19fd0bb4 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt @@ -12,25 +12,27 @@ import android.app.Activity import android.content.Intent import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle -import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2Delegate import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.Threeds2ChallengeAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionObserverRepository +import com.adyen.checkout.components.core.internal.PermissionRequestData import com.adyen.checkout.components.core.internal.ui.ActionDelegate import com.adyen.checkout.components.core.internal.ui.DetailsEmittingDelegate import com.adyen.checkout.components.core.internal.ui.IntentHandlingDelegate +import com.adyen.checkout.components.core.internal.ui.PermissionRequestingDelegate import com.adyen.checkout.components.core.internal.ui.RedirectableDelegate import com.adyen.checkout.components.core.internal.ui.StatusPollingDelegate import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.internal.util.repeatOnResume +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runCompileOnly import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate @@ -46,7 +48,7 @@ import kotlinx.coroutines.flow.receiveAsFlow internal class DefaultGenericActionDelegate( private val observerRepository: ActionObserverRepository, private val savedStateHandle: SavedStateHandle, - private val configuration: GenericActionConfiguration, + private val checkoutConfiguration: CheckoutConfiguration, override val componentParams: GenericComponentParams, private val actionDelegateProvider: ActionDelegateProvider, ) : GenericActionDelegate { @@ -60,16 +62,19 @@ internal class DefaultGenericActionDelegate( private var _coroutineScope: CoroutineScope? = null private val coroutineScope: CoroutineScope get() = requireNotNull(_coroutineScope) + private val detailsChannel: Channel = bufferedChannel() + override val detailsFlow: Flow = detailsChannel.receiveAsFlow() + private val exceptionChannel: Channel = bufferedChannel() override val exceptionFlow: Flow = exceptionChannel.receiveAsFlow() - private val detailsChannel: Channel = bufferedChannel() - override val detailsFlow: Flow = detailsChannel.receiveAsFlow() + private val permissionChannel: Channel = bufferedChannel() + override val permissionFlow: Flow = permissionChannel.receiveAsFlow() private var onRedirectListener: (() -> Unit)? = null override fun initialize(coroutineScope: CoroutineScope) { - Logger.d(TAG, "initialize") + adyenLog(AdyenLogLevel.DEBUG) { "initialize" } _coroutineScope = coroutineScope } @@ -81,9 +86,10 @@ internal class DefaultGenericActionDelegate( observerRepository.addObservers( detailsFlow = detailsFlow, exceptionFlow = exceptionFlow, + permissionFlow = permissionFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) // Immediately request a new status if the user resumes the app @@ -100,16 +106,16 @@ internal class DefaultGenericActionDelegate( // During this whole flow the same transaction instance should be used for both fingerprint and challenge. // Therefore we are making sure the same delegate persists when handleAction is called again. if (isOld3DS2Flow(action)) { - Logger.d(TAG, "Continuing the handling of 3ds2 challenge with old flow.") + adyenLog(AdyenLogLevel.DEBUG) { "Continuing the handling of 3ds2 challenge with old flow." } } else { val delegate = actionDelegateProvider.getDelegate( action = action, - configuration = configuration, + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, - application = activity.application + application = activity.application, ) this._delegate = delegate - Logger.d(TAG, "Created delegate of type ${delegate::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "Created delegate of type ${delegate::class.simpleName}" } if (delegate is RedirectableDelegate) { onRedirectListener?.let { delegate.setOnRedirectListener(it) } @@ -119,6 +125,7 @@ internal class DefaultGenericActionDelegate( observeDetails(delegate) observeExceptions(delegate) + observePermissionRequests(delegate) observeViewFlow(delegate) } @@ -129,24 +136,32 @@ internal class DefaultGenericActionDelegate( return runCompileOnly { _delegate is Adyen3DS2Delegate && action is Threeds2ChallengeAction } ?: false } + private fun observeDetails(delegate: ActionDelegate) { + if (delegate !is DetailsEmittingDelegate) return + adyenLog(AdyenLogLevel.DEBUG) { "Observing details" } + delegate.detailsFlow + .onEach { detailsChannel.trySend(it) } + .launchIn(coroutineScope) + } + private fun observeExceptions(delegate: ActionDelegate) { - Logger.d(TAG, "Observing exceptions") + adyenLog(AdyenLogLevel.DEBUG) { "Observing exceptions" } delegate.exceptionFlow .onEach { exceptionChannel.trySend(it) } .launchIn(coroutineScope) } - private fun observeDetails(delegate: ActionDelegate) { - if (delegate !is DetailsEmittingDelegate) return - Logger.d(TAG, "Observing details") - delegate.detailsFlow - .onEach { detailsChannel.trySend(it) } + private fun observePermissionRequests(delegate: ActionDelegate) { + if (delegate !is PermissionRequestingDelegate) return + adyenLog(AdyenLogLevel.DEBUG) { "Observing permission requests" } + delegate.permissionFlow + .onEach { permissionChannel.trySend(it) } .launchIn(coroutineScope) } private fun observeViewFlow(delegate: ActionDelegate) { if (delegate !is ViewProvidingDelegate) return - Logger.d(TAG, "Observing view flow") + adyenLog(AdyenLogLevel.DEBUG) { "Observing view flow" } delegate.viewFlow .onEach { _viewFlow.tryEmit(it) } .launchIn(coroutineScope) @@ -163,7 +178,7 @@ internal class DefaultGenericActionDelegate( } else -> { - Logger.d(TAG, "Handling intent") + adyenLog(AdyenLogLevel.DEBUG) { "Handling intent" } delegate.handleIntent(intent) } } @@ -172,7 +187,7 @@ internal class DefaultGenericActionDelegate( override fun refreshStatus() { val delegate = _delegate if (delegate !is StatusPollingDelegate) return - Logger.d(TAG, "Refreshing status") + adyenLog(AdyenLogLevel.DEBUG) { "Refreshing status" } delegate.refreshStatus() } @@ -185,15 +200,11 @@ internal class DefaultGenericActionDelegate( } override fun onCleared() { - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } removeObserver() _delegate?.onCleared() _delegate = null _coroutineScope = null onRedirectListener = null } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt index df3fcd0447..aebe2e937f 100644 --- a/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt +++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt @@ -12,6 +12,7 @@ import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.internal.ui.ActionDelegate import com.adyen.checkout.components.core.internal.ui.DetailsEmittingDelegate import com.adyen.checkout.components.core.internal.ui.IntentHandlingDelegate +import com.adyen.checkout.components.core.internal.ui.PermissionRequestingDelegate import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -19,7 +20,8 @@ interface GenericActionDelegate : ActionDelegate, DetailsEmittingDelegate, IntentHandlingDelegate, - ViewProvidingDelegate { + ViewProvidingDelegate, + PermissionRequestingDelegate { val delegate: ActionDelegate diff --git a/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt index efc6412557..50e9e0316b 100644 --- a/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt +++ b/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt @@ -8,20 +8,20 @@ package com.adyen.checkout.action.core +import android.app.Activity +import android.content.Intent import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewModelScope import app.cash.turbine.test -import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.action.AwaitAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler import com.adyen.checkout.components.core.internal.ui.ActionDelegate -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -35,12 +35,10 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class GenericActionComponentTest( @Mock private val actionDelegate: ActionDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, - @Mock private val actionHandlingComponent: DefaultActionHandlingComponent, @Mock private val actionComponentEventHandler: ActionComponentEventHandler, ) { @@ -48,11 +46,9 @@ internal class GenericActionComponentTest( @BeforeEach fun before() { - AdyenLogger.setLogLevel(Logger.NONE) - whenever(genericActionDelegate.delegate) doReturn actionDelegate whenever(genericActionDelegate.viewFlow) doReturn MutableStateFlow(TestComponentViewType.VIEW_TYPE_1) - component = GenericActionComponent(genericActionDelegate, actionHandlingComponent, actionComponentEventHandler) + component = GenericActionComponent(genericActionDelegate, actionComponentEventHandler) } @Test @@ -101,7 +97,7 @@ internal class GenericActionComponentTest( fun `when delegate view flow emits a value then component view flow should match that value`() = runTest { val delegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1) whenever(genericActionDelegate.viewFlow) doReturn delegateViewFlow - component = GenericActionComponent(genericActionDelegate, actionHandlingComponent, actionComponentEventHandler) + component = GenericActionComponent(genericActionDelegate, actionComponentEventHandler) component.viewFlow.test { assertEquals(TestComponentViewType.VIEW_TYPE_1, awaitItem()) @@ -112,4 +108,32 @@ internal class GenericActionComponentTest( expectNoEvents() } } + + @Test + fun `when handleAction is called then handleAction in delegate is called`() { + val action = AwaitAction() + val activity = Activity() + + component.handleAction(action, activity) + + verify(genericActionDelegate).handleAction(action, activity) + } + + @Test + fun `when handleIntent is called then handleIntent in delegate is called`() { + val intent = Intent() + + component.handleIntent(intent) + + verify(genericActionDelegate).handleIntent(intent) + } + + @Test + fun `when setOnRedirectListener is called then setOnRedirectListener in delegate is called`() { + val listener = { } + + component.setOnRedirectListener(listener) + + verify(genericActionDelegate).setOnRedirectListener(listener) + } } diff --git a/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionConfigurationTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionConfigurationTest.kt new file mode 100644 index 0000000000..a608c7978a --- /dev/null +++ b/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionConfigurationTest.kt @@ -0,0 +1,77 @@ +package com.adyen.checkout.action.core + +import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration +import com.adyen.checkout.await.AwaitConfiguration +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.qrcode.QRCodeConfiguration +import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.voucher.VoucherConfiguration +import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class GenericActionConfigurationTest { + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = GenericActionConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .add3ds2ActionConfiguration( + Adyen3DS2Configuration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addAwaitActionConfiguration( + AwaitConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addQRCodeActionConfiguration( + QRCodeConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addRedirectActionConfiguration( + RedirectConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addVoucherActionConfiguration( + VoucherConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addWeChatPayActionConfiguration( + WeChatPayActionConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + assertNotNull(actual.getActionConfiguration(Adyen3DS2Configuration::class.java)) + assertNotNull(actual.getActionConfiguration(AwaitConfiguration::class.java)) + assertNotNull(actual.getActionConfiguration(QRCodeConfiguration::class.java)) + assertNotNull(actual.getActionConfiguration(RedirectConfiguration::class.java)) + assertNotNull(actual.getActionConfiguration(VoucherConfiguration::class.java)) + assertNotNull(actual.getActionConfiguration(WeChatPayActionConfiguration::class.java)) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt b/action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt index 8f5edbf825..1093d326f7 100644 --- a/action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt +++ b/action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt @@ -10,21 +10,18 @@ package com.adyen.checkout.action.core import android.app.Activity import android.content.Intent -import android.os.Parcel import androidx.lifecycle.LifecycleOwner -import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2Delegate import com.adyen.checkout.components.core.ActionComponentData -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.ActionComponentEvent -import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.components.core.internal.ui.ActionDelegate import com.adyen.checkout.components.core.internal.ui.DetailsEmittingDelegate import com.adyen.checkout.components.core.internal.ui.IntentHandlingDelegate import com.adyen.checkout.components.core.internal.ui.StatusPollingDelegate import com.adyen.checkout.components.core.internal.ui.ViewableDelegate +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.OutputData @@ -52,8 +49,8 @@ internal class TestActionDelegate : QRCodeOutputData( isValid = false, paymentMethodType = null, - qrCodeData = null - ) + qrCodeData = null, + ), ) override val outputData: QRCodeOutputData get() = outputDataFlow.value @@ -66,23 +63,15 @@ internal class TestActionDelegate : override val viewFlow: MutableStateFlow = MutableStateFlow(null) - private val configuration: Configuration = object : Configuration { - override val shopperLocale: Locale = Locale.US - override val environment: Environment = Environment.TEST - override val clientKey: String = "" - override val analyticsConfiguration: AnalyticsConfiguration? = null - override val amount: Amount? = null - - override fun describeContents(): Int { - throw NotImplementedError("This method shouldn't be used in tests") - } - - override fun writeToParcel(dest: Parcel, flags: Int) { - throw NotImplementedError("This method shouldn't be used in tests") - } - } - override val componentParams: ComponentParams = - GenericComponentParamsMapper(null, null).mapToParams(configuration, null) + private val configuration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = "", + amount = null, + analyticsConfiguration = null, + ) + override val componentParams: ComponentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null) var initializeCalled = false override fun initialize(coroutineScope: CoroutineScope) { @@ -120,11 +109,14 @@ internal class TestActionDelegate : internal class Test3DS2Delegate : Adyen3DS2Delegate { - private val configuration: Adyen3DS2Configuration = - Adyen3DS2Configuration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build() + private val configuration: CheckoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) - override val componentParams: ComponentParams = - GenericComponentParamsMapper(null, null).mapToParams(configuration, null) + override val componentParams: ComponentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null) override val detailsFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) diff --git a/action-core/src/test/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponentTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponentTest.kt new file mode 100644 index 0000000000..58fdf6edb3 --- /dev/null +++ b/action-core/src/test/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponentTest.kt @@ -0,0 +1,81 @@ +package com.adyen.checkout.action.core.internal + +import android.app.Activity +import android.content.Intent +import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.action.AwaitAction +import com.adyen.checkout.components.core.internal.ui.ActionDelegate +import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@ExtendWith(MockitoExtension::class) +internal class DefaultActionHandlingComponentTest( + @Mock private val genericActionDelegate: GenericActionDelegate, + @Mock private val paymentDelegate: PaymentComponentDelegate<*>, +) { + + private lateinit var actionHandlingComponent: DefaultActionHandlingComponent + + @BeforeEach + fun setup() { + actionHandlingComponent = DefaultActionHandlingComponent( + genericActionDelegate = genericActionDelegate, + paymentDelegate = paymentDelegate, + ) + } + + @Test + fun `when getting active delegate before handling an action, then the payment delegate should be returned`() { + val activeDelegate = actionHandlingComponent.activeDelegate + + assertEquals(paymentDelegate, activeDelegate) + } + + @Test + fun `when getting active delegate after handling an action, then the generic action delegate should be returned`() { + val mockActionDelegate = mock() + whenever(genericActionDelegate.delegate) doReturn mockActionDelegate + actionHandlingComponent.handleAction(AwaitAction(), Activity()) + + val activeDelegate = actionHandlingComponent.activeDelegate + + assertEquals(mockActionDelegate, activeDelegate) + } + + @Test + fun `when handleAction is called then handleAction in delegate is called`() { + val action = AwaitAction() + val activity = Activity() + + actionHandlingComponent.handleAction(action, activity) + + verify(genericActionDelegate).handleAction(action, activity) + } + + @Test + fun `when handleIntent is called then handleIntent in delegate is called`() { + val intent = Intent() + + actionHandlingComponent.handleIntent(intent) + + verify(genericActionDelegate).handleIntent(intent) + } + + @Test + fun `when setOnRedirectListener is called then setOnRedirectListener in delegate is called`() { + val listener = { } + + actionHandlingComponent.setOnRedirectListener(listener) + + verify(genericActionDelegate).setOnRedirectListener(listener) + } +} diff --git a/action-core/src/test/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProviderTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProviderTest.kt new file mode 100644 index 0000000000..6cd18f7d04 --- /dev/null +++ b/action-core/src/test/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProviderTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.action.core.internal.ui + +import android.app.Application +import android.os.Parcel +import androidx.lifecycle.SavedStateHandle +import com.adyen.checkout.adyen3ds2.internal.ui.Adyen3DS2Delegate +import com.adyen.checkout.await.internal.ui.AwaitDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.components.core.action.AwaitAction +import com.adyen.checkout.components.core.action.QrCodeAction +import com.adyen.checkout.components.core.action.RedirectAction +import com.adyen.checkout.components.core.action.SdkAction +import com.adyen.checkout.components.core.action.Threeds2Action +import com.adyen.checkout.components.core.action.Threeds2ChallengeAction +import com.adyen.checkout.components.core.action.Threeds2FingerprintAction +import com.adyen.checkout.components.core.action.VoucherAction +import com.adyen.checkout.components.core.action.WeChatPaySdkData +import com.adyen.checkout.components.core.internal.ui.ActionDelegate +import com.adyen.checkout.core.Environment +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.internal.util.LocaleProvider +import com.adyen.checkout.qrcode.internal.ui.QRCodeDelegate +import com.adyen.checkout.redirect.internal.ui.RedirectDelegate +import com.adyen.checkout.voucher.internal.ui.VoucherDelegate +import com.adyen.checkout.wechatpay.internal.ui.WeChatDelegate +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever +import java.util.Locale + +@ExtendWith(MockitoExtension::class) +internal class ActionDelegateProviderTest( + @Mock private val localeProvider: LocaleProvider +) { + + private lateinit var actionDelegateProvider: ActionDelegateProvider + + @BeforeEach + fun setup() { + whenever(localeProvider.getLocale(any())) doReturn Locale.US + actionDelegateProvider = ActionDelegateProvider(null, localeProvider) + } + + @ParameterizedTest + @MethodSource("actionSource") + fun `when action is of certain type, then related delegate is provided`( + action: Action, + expectedDelegate: Class, + ) { + val configuration = CheckoutConfiguration(Environment.TEST, "") + + val delegate = actionDelegateProvider.getDelegate(action, configuration, SavedStateHandle(), Application()) + + assertInstanceOf(expectedDelegate, delegate) + } + + @Test + fun `when unknown action is used, then an error will be thrown`() { + val configuration = CheckoutConfiguration(Environment.TEST, "") + + assertThrows { + actionDelegateProvider.getDelegate(UnknownAction(), configuration, SavedStateHandle(), Application()) + } + } + + companion object { + + @JvmStatic + fun actionSource() = listOf( + arguments(AwaitAction(), AwaitDelegate::class.java), + arguments(QrCodeAction(), QRCodeDelegate::class.java), + arguments(RedirectAction(), RedirectDelegate::class.java), + arguments(Threeds2Action(), Adyen3DS2Delegate::class.java), + arguments(Threeds2ChallengeAction(), Adyen3DS2Delegate::class.java), + arguments(Threeds2FingerprintAction(), Adyen3DS2Delegate::class.java), + arguments(VoucherAction(), VoucherDelegate::class.java), + arguments(SdkAction(), WeChatDelegate::class.java), + ) + } + + private class UnknownAction( + override var type: String? = null, + override var paymentMethodType: String? = null, + override var paymentData: String? = null, + ) : Action() { + override fun writeToParcel(dest: Parcel, flags: Int) = Unit + } +} diff --git a/action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt index d82f24111b..08097e92ba 100644 --- a/action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt +++ b/action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt @@ -13,22 +13,22 @@ import android.app.Application import android.content.Intent import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.Test3DS2Delegate import com.adyen.checkout.action.core.TestActionDelegate import com.adyen.checkout.action.core.internal.ui.ActionDelegateProvider import com.adyen.checkout.action.core.internal.ui.DefaultGenericActionDelegate import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.action.Threeds2ChallengeAction import com.adyen.checkout.components.core.action.Threeds2FingerprintAction import com.adyen.checkout.components.core.internal.ActionObserverRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.ui.core.internal.test.TestComponentViewType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -48,7 +48,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultGenericActionDelegateTest( @Mock private val activity: Activity, @Mock private val actionDelegateProvider: ActionDelegateProvider, @@ -58,24 +58,23 @@ internal class DefaultGenericActionDelegateTest( @BeforeEach fun beforeEach() { - val configuration = GenericActionConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ).build() + val configuration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) genericActionDelegate = DefaultGenericActionDelegate( ActionObserverRepository(), SavedStateHandle(), configuration, - GenericComponentParamsMapper(null, null).mapToParams(configuration, null), - actionDelegateProvider + GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), + actionDelegateProvider, ) whenever(activity.application) doReturn Application() testDelegate = TestActionDelegate() whenever(actionDelegateProvider.getDelegate(any(), any(), any(), any())) doReturn testDelegate - - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -214,7 +213,7 @@ internal class DefaultGenericActionDelegateTest( fun `when handleAction is called with a Threeds2ChallengeAction the inner delegate is not re-created`() = runTest { val adyen3DS2Delegate = Test3DS2Delegate() whenever( - actionDelegateProvider.getDelegate(any(), any(), any(), any()) + actionDelegateProvider.getDelegate(any(), any(), any(), any()), ) doReturn adyen3DS2Delegate genericActionDelegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) diff --git a/await/src/main/java/com/adyen/checkout/await/AwaitComponent.kt b/await/src/main/java/com/adyen/checkout/await/AwaitComponent.kt index 9d30136862..401719bd85 100644 --- a/await/src/main/java/com/adyen/checkout/await/AwaitComponent.kt +++ b/await/src/main/java/com/adyen/checkout/await/AwaitComponent.kt @@ -18,8 +18,8 @@ import com.adyen.checkout.components.core.internal.ActionComponent import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent import kotlinx.coroutines.flow.Flow @@ -58,12 +58,11 @@ class AwaitComponent internal constructor( override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } delegate.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER: ActionComponentProvider = diff --git a/await/src/main/java/com/adyen/checkout/await/AwaitConfiguration.kt b/await/src/main/java/com/adyen/checkout/await/AwaitConfiguration.kt index 8922b1d87f..6848d98ecb 100644 --- a/await/src/main/java/com/adyen/checkout/await/AwaitConfiguration.kt +++ b/await/src/main/java/com/adyen/checkout/await/AwaitConfiguration.kt @@ -10,8 +10,10 @@ package com.adyen.checkout.await import android.content.Context import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -21,7 +23,7 @@ import java.util.Locale */ @Parcelize class AwaitConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -33,6 +35,22 @@ class AwaitConfiguration private constructor( */ class Builder : BaseConfigurationBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -40,14 +58,15 @@ class AwaitConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -56,7 +75,7 @@ class AwaitConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) override fun buildInternal(): AwaitConfiguration { @@ -70,3 +89,34 @@ class AwaitConfiguration private constructor( } } } + +fun CheckoutConfiguration.await( + configuration: @CheckoutConfigurationMarker AwaitConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = AwaitConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addActionConfiguration(config) + return this +} + +fun CheckoutConfiguration.getAwaitConfiguration(): AwaitConfiguration? { + return getActionConfiguration(AwaitConfiguration::class.java) +} + +internal fun AwaitConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addActionConfiguration(this@toCheckoutConfiguration) + } +} diff --git a/await/src/main/java/com/adyen/checkout/await/internal/provider/AwaitComponentProvider.kt b/await/src/main/java/com/adyen/checkout/await/internal/provider/AwaitComponentProvider.kt index c43dfee339..41ec02bb66 100644 --- a/await/src/main/java/com/adyen/checkout/await/internal/provider/AwaitComponentProvider.kt +++ b/await/src/main/java/com/adyen/checkout/await/internal/provider/AwaitComponentProvider.kt @@ -19,7 +19,9 @@ import com.adyen.checkout.await.AwaitComponent import com.adyen.checkout.await.AwaitConfiguration import com.adyen.checkout.await.internal.ui.AwaitDelegate import com.adyen.checkout.await.internal.ui.DefaultAwaitDelegate +import com.adyen.checkout.await.toCheckoutConfiguration import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.AwaitAction @@ -29,22 +31,21 @@ import com.adyen.checkout.components.core.internal.PaymentDataRepository import com.adyen.checkout.components.core.internal.data.api.DefaultStatusRepository import com.adyen.checkout.components.core.internal.data.api.StatusService import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider class AwaitComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : ActionComponentProvider { - private val componentParamsMapper = GenericComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override val supportedActionTypes: List get() = listOf(AwaitAction.ACTION_TYPE) @@ -53,15 +54,15 @@ constructor( viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, application: Application, - configuration: AwaitConfiguration, + checkoutConfiguration: CheckoutConfiguration, callback: ActionComponentCallback, - key: String?, + key: String? ): AwaitComponent { val awaitFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val awaitDelegate = getDelegate(configuration, savedStateHandle, application) + val awaitDelegate = getDelegate(checkoutConfiguration, savedStateHandle, application) AwaitComponent( awaitDelegate, - DefaultActionComponentEventHandler(callback) + DefaultActionComponentEventHandler(callback), ) } return ViewModelProvider(viewModelStoreOwner, awaitFactory)[key, AwaitComponent::class.java].also { component -> @@ -70,20 +71,46 @@ constructor( } override fun getDelegate( - configuration: AwaitConfiguration, + checkoutConfiguration: CheckoutConfiguration, savedStateHandle: SavedStateHandle, - application: Application, + application: Application ): AwaitDelegate { - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val statusService = StatusService(httpClient) - val statusRepository = DefaultStatusRepository(statusService, configuration.clientKey) + val statusRepository = DefaultStatusRepository(statusService, componentParams.clientKey) val paymentDataRepository = PaymentDataRepository(savedStateHandle) return DefaultAwaitDelegate( observerRepository = ActionObserverRepository(), componentParams = componentParams, statusRepository = statusRepository, - paymentDataRepository = paymentDataRepository + paymentDataRepository = paymentDataRepository, + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + configuration: AwaitConfiguration, + callback: ActionComponentCallback, + key: String?, + ): AwaitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + application = application, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + key = key, ) } diff --git a/await/src/main/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegate.kt b/await/src/main/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegate.kt index 7b3321201f..cbd5b04ee3 100644 --- a/await/src/main/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegate.kt +++ b/await/src/main/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegate.kt @@ -25,10 +25,10 @@ import com.adyen.checkout.components.core.internal.ui.model.TimerData import com.adyen.checkout.components.core.internal.util.StatusResponseUtils import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.internal.util.repeatOnResume +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -84,9 +84,10 @@ internal class DefaultAwaitDelegate( observerRepository.addObservers( detailsFlow = detailsFlow, exceptionFlow = exceptionFlow, + permissionFlow = null, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) // Immediately request a new status if the user resumes the app @@ -106,7 +107,7 @@ internal class DefaultAwaitDelegate( val paymentData = action.paymentData paymentDataRepository.paymentData = paymentData if (paymentData == null) { - Logger.e(TAG, "Payment data is null") + adyenLog(AdyenLogLevel.ERROR) { "Payment data is null" } exceptionChannel.trySend(ComponentException("Payment data is null")) return } @@ -124,16 +125,16 @@ internal class DefaultAwaitDelegate( private fun onStatus(result: Result, action: Action) { result.fold( onSuccess = { response -> - Logger.v(TAG, "Status changed - ${response.resultCode}") + adyenLog(AdyenLogLevel.VERBOSE) { "Status changed - ${response.resultCode}" } createOutputData(response, action) if (StatusResponseUtils.isFinalResult(response)) { onPollingSuccessful(response) } }, onFailure = { - Logger.e(TAG, "Error while polling status", it) + adyenLog(AdyenLogLevel.ERROR, it) { "Error while polling status" } exceptionChannel.trySend(ComponentException("Error while polling status", it)) - } + }, ) } @@ -145,7 +146,7 @@ internal class DefaultAwaitDelegate( private fun createOutputData() = AwaitOutputData( isValid = false, - paymentMethodType = null + paymentMethodType = null, ) private fun onPollingSuccessful(statusResponse: StatusResponse) { @@ -189,8 +190,6 @@ internal class DefaultAwaitDelegate( } companion object { - private val TAG = LogUtil.getTag() - private val DEFAULT_MAX_POLLING_DURATION = TimeUnit.MINUTES.toMillis(15) @VisibleForTesting diff --git a/await/src/main/java/com/adyen/checkout/await/internal/ui/view/AwaitView.kt b/await/src/main/java/com/adyen/checkout/await/internal/ui/view/AwaitView.kt index ee6e9c51c8..9ee905df96 100644 --- a/await/src/main/java/com/adyen/checkout/await/internal/ui/view/AwaitView.kt +++ b/await/src/main/java/com/adyen/checkout/await/internal/ui/view/AwaitView.kt @@ -19,8 +19,8 @@ import com.adyen.checkout.await.internal.ui.AwaitDelegate import com.adyen.checkout.await.internal.ui.model.AwaitOutputData import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.ui.loadLogo import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle @@ -36,7 +36,7 @@ internal class AwaitView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -66,7 +66,7 @@ internal class AwaitView @JvmOverloads constructor( private fun initLocalizedStrings(localizedContext: Context) { binding.textViewWaitingConfirmation.setLocalizedTextFromStyle( R.style.AdyenCheckout_Await_WaitingConfirmationTextView, - localizedContext + localizedContext, ) } @@ -77,7 +77,7 @@ internal class AwaitView @JvmOverloads constructor( } private fun outputDataChanged(outputData: AwaitOutputData) { - Logger.d(TAG, "outputDataChanged") + adyenLog(AdyenLogLevel.DEBUG) { "outputDataChanged" } updateMessageText(outputData.paymentMethodType) updateLogo(outputData.paymentMethodType) @@ -94,7 +94,7 @@ internal class AwaitView @JvmOverloads constructor( } private fun updateLogo(paymentMethodType: String?) { - Logger.d(TAG, "updateLogo - $paymentMethodType") + adyenLog(AdyenLogLevel.DEBUG) { "updateLogo - $paymentMethodType" } paymentMethodType?.let { txVariant -> binding.imageViewLogo.loadLogo( environment = delegate.componentParams.environment, @@ -114,8 +114,4 @@ internal class AwaitView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/await/src/test/java/com/adyen/checkout/await/AwaitComponentTest.kt b/await/src/test/java/com/adyen/checkout/await/AwaitComponentTest.kt index ac826f548c..89a1e1d96a 100644 --- a/await/src/test/java/com/adyen/checkout/await/AwaitComponentTest.kt +++ b/await/src/test/java/com/adyen/checkout/await/AwaitComponentTest.kt @@ -17,8 +17,7 @@ import com.adyen.checkout.await.internal.ui.AwaitDelegate import com.adyen.checkout.components.core.action.AwaitAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -37,7 +36,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class AwaitComponentTest( @Mock private val awaitDelegate: AwaitDelegate, @Mock private val actionComponentEventHandler: ActionComponentEventHandler, @@ -47,8 +46,6 @@ internal class AwaitComponentTest( @BeforeEach fun before() { - AdyenLogger.setLogLevel(Logger.NONE) - whenever(awaitDelegate.viewFlow) doReturn MutableStateFlow(AwaitComponentViewType) component = AwaitComponent(awaitDelegate, actionComponentEventHandler) } diff --git a/await/src/test/java/com/adyen/checkout/await/AwaitConfigurationTest.kt b/await/src/test/java/com/adyen/checkout/await/AwaitConfigurationTest.kt new file mode 100644 index 0000000000..f7e6a1ba11 --- /dev/null +++ b/await/src/test/java/com/adyen/checkout/await/AwaitConfigurationTest.kt @@ -0,0 +1,82 @@ +package com.adyen.checkout.await + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class AwaitConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + await() + } + + val actual = checkoutConfiguration.getAwaitConfiguration() + + val expected = AwaitConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = AwaitConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualAwaitConfig = actual.getAwaitConfiguration() + assertEquals(config.shopperLocale, actualAwaitConfig?.shopperLocale) + assertEquals(config.environment, actualAwaitConfig?.environment) + assertEquals(config.clientKey, actualAwaitConfig?.clientKey) + assertEquals(config.amount, actualAwaitConfig?.amount) + assertEquals(config.analyticsConfiguration, actualAwaitConfig?.analyticsConfiguration) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/await/src/test/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegateTest.kt b/await/src/test/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegateTest.kt index c5e3aac94a..f609006dfc 100644 --- a/await/src/test/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegateTest.kt +++ b/await/src/test/java/com/adyen/checkout/await/internal/ui/DefaultAwaitDelegateTest.kt @@ -11,17 +11,17 @@ package com.adyen.checkout.await.internal.ui import android.app.Activity import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test -import com.adyen.checkout.await.AwaitConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.AwaitAction import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository import com.adyen.checkout.components.core.internal.data.model.StatusResponse import com.adyen.checkout.components.core.internal.test.TestStatusRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -32,10 +32,12 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import java.io.IOException import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(LoggingExtension::class) internal class DefaultAwaitDelegateTest { private lateinit var statusRepository: TestStatusRepository @@ -46,18 +48,17 @@ internal class DefaultAwaitDelegateTest { fun beforeEach() { statusRepository = TestStatusRepository() paymentDataRepository = PaymentDataRepository(SavedStateHandle()) - val configuration = AwaitConfiguration.Builder( - Locale.US, + val configuration = CheckoutConfiguration( Environment.TEST, - TEST_CLIENT_KEY - ).build() + TEST_CLIENT_KEY, + ) delegate = DefaultAwaitDelegate( ActionObserverRepository(), - GenericComponentParamsMapper(null, null).mapToParams(configuration, null), + GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), statusRepository, - paymentDataRepository + paymentDataRepository, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitComponent.kt b/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitComponent.kt index e3928f6023..e369ae77b8 100644 --- a/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitComponent.kt +++ b/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitComponent.kt @@ -23,8 +23,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent @@ -103,24 +103,24 @@ class BacsDirectDebitComponent internal constructor( override fun isConfirmationRequired(): Boolean = bacsDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? BacsDirectDebitDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } bacsDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = BacsDirectDebitComponentProvider() diff --git a/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitConfiguration.kt b/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitConfiguration.kt index d74f20df88..a4a1d40697 100644 --- a/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitConfiguration.kt +++ b/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitConfiguration.kt @@ -13,9 +13,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -26,7 +29,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class BacsDirectDebitConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -44,6 +47,22 @@ class BacsDirectDebitConfiguration private constructor( private var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -51,14 +70,15 @@ class BacsDirectDebitConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -67,7 +87,7 @@ class BacsDirectDebitConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -95,3 +115,38 @@ class BacsDirectDebitConfiguration private constructor( } } } + +fun CheckoutConfiguration.bacsDirectDebit( + configuration: @CheckoutConfigurationMarker BacsDirectDebitConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = BacsDirectDebitConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.BACS, config) + return this +} + +fun CheckoutConfiguration.getBacsDirectDebitConfiguration(): BacsDirectDebitConfiguration? { + return getConfiguration(PaymentMethodTypes.BACS) +} + +internal fun BacsDirectDebitConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.BACS, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/bacs/src/main/java/com/adyen/checkout/bacs/internal/provider/BacsDirectDebitComponentProvider.kt b/bacs/src/main/java/com/adyen/checkout/bacs/internal/provider/BacsDirectDebitComponentProvider.kt index 2106f96804..d3b02aea63 100644 --- a/bacs/src/main/java/com/adyen/checkout/bacs/internal/provider/BacsDirectDebitComponentProvider.kt +++ b/bacs/src/main/java/com/adyen/checkout/bacs/internal/provider/BacsDirectDebitComponentProvider.kt @@ -19,7 +19,10 @@ import com.adyen.checkout.action.core.internal.provider.GenericActionComponentPr import com.adyen.checkout.bacs.BacsDirectDebitComponent import com.adyen.checkout.bacs.BacsDirectDebitComponentState import com.adyen.checkout.bacs.BacsDirectDebitConfiguration +import com.adyen.checkout.bacs.getBacsDirectDebitConfiguration import com.adyen.checkout.bacs.internal.ui.DefaultBacsDirectDebitDelegate +import com.adyen.checkout.bacs.toCheckoutConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -32,12 +35,13 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -52,31 +56,29 @@ import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class BacsDirectDebitComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< BacsDirectDebitComponent, BacsDirectDebitConfiguration, BacsDirectDebitComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< BacsDirectDebitComponent, BacsDirectDebitConfiguration, BacsDirectDebitComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: BacsDirectDebitConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -85,7 +87,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = checkoutConfiguration.getBacsDirectDebitConfiguration(), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -94,7 +102,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -108,8 +116,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -118,7 +126,7 @@ constructor( bacsDelegate = bacsDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, bacsDelegate), - componentEventHandler = DefaultComponentEventHandler() + componentEventHandler = DefaultComponentEventHandler(), ) } @@ -130,6 +138,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: BacsDirectDebitConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): BacsDirectDebitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -137,7 +169,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: BacsDirectDebitConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -145,10 +177,14 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = checkoutConfiguration.getBacsDirectDebitConfiguration(), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -159,7 +195,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -173,8 +209,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -189,20 +225,21 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer + sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, ) BacsDirectDebitComponent( bacsDelegate = bacsDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, bacsDelegate), - componentEventHandler = sessionComponentEventHandler + componentEventHandler = sessionComponentEventHandler, ) } + return ViewModelProvider(viewModelStoreOwner, genericFactory)[key, BacsDirectDebitComponent::class.java] .also { component -> component.observe(lifecycleOwner) { @@ -211,6 +248,31 @@ constructor( } } + @Suppress("LongMethod") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: BacsDirectDebitConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): BacsDirectDebitComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/DefaultBacsDirectDebitDelegate.kt b/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/DefaultBacsDirectDebitDelegate.kt index 6db7f21f55..3d0c507ff0 100644 --- a/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/DefaultBacsDirectDebitDelegate.kt +++ b/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/DefaultBacsDirectDebitDelegate.kt @@ -23,8 +23,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParams import com.adyen.checkout.components.core.paymentmethod.BacsDirectDebitPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent @@ -70,7 +70,7 @@ internal class DefaultBacsDirectDebitDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -101,17 +101,17 @@ internal class DefaultBacsDirectDebitDelegate( val currentMode = inputData.mode return when { mode == currentMode -> { - Logger.e(TAG, "Current mode is already $mode") + adyenLog(AdyenLogLevel.ERROR) { "Current mode is already $mode" } false } mode == BacsDirectDebitMode.CONFIRMATION && !outputData.isValid -> { - Logger.e(TAG, "Cannot set confirmation view when input is not valid") + adyenLog(AdyenLogLevel.ERROR) { "Cannot set confirmation view when input is not valid" } false } else -> { - Logger.d(TAG, "Setting mode to $mode") + adyenLog(AdyenLogLevel.DEBUG) { "Setting mode to $mode" } updateInputData { this.mode = mode } true } @@ -165,7 +165,7 @@ internal class DefaultBacsDirectDebitDelegate( BacsDirectDebitMode.CONFIRMATION -> BacsComponentViewType.CONFIRMATION } if (_viewFlow.value != viewType) { - Logger.d(TAG, "Updating view flow to $viewType") + adyenLog(AdyenLogLevel.DEBUG) { "Updating view flow to $viewType" } _viewFlow.tryEmit(viewType) } } @@ -208,7 +208,7 @@ internal class DefaultBacsDirectDebitDelegate( data = paymentComponentData, isInputValid = outputData.isValid, isReady = true, - mode = outputData.mode + mode = outputData.mode, ) } @@ -223,8 +223,4 @@ internal class DefaultBacsDirectDebitDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitInputView.kt b/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitInputView.kt index 56730eb014..09c52a28ef 100644 --- a/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitInputView.kt +++ b/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitInputView.kt @@ -24,8 +24,8 @@ import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParam import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.CurrencyUtils -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.ui.view.AdyenTextInputEditText import com.adyen.checkout.ui.core.internal.util.hideError @@ -45,7 +45,7 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -105,7 +105,7 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( binding.editTextBankAccountNumber.requestFocus() } binding.textInputLayoutBankAccountNumber.showError( - localizedContext.getString(bankAccountNumberValidation.reason) + localizedContext.getString(bankAccountNumberValidation.reason), ) } val sortCodeValidation = it.sortCodeState.validation @@ -144,23 +144,23 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( private fun initLocalizedStrings(localizedContext: Context) { binding.textInputLayoutHolderName.setLocalizedHintFromStyle( R.style.AdyenCheckout_Bacs_HolderNameInput, - localizedContext + localizedContext, ) binding.textInputLayoutBankAccountNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_Bacs_AccountNumberInput, - localizedContext + localizedContext, ) binding.textInputLayoutSortCode.setLocalizedHintFromStyle( R.style.AdyenCheckout_Bacs_SortCodeInput, - localizedContext + localizedContext, ) binding.textInputLayoutShopperEmail.setLocalizedHintFromStyle( R.style.AdyenCheckout_Bacs_ShopperEmailInput, - localizedContext + localizedContext, ) binding.switchConsentAccount.setLocalizedTextFromStyle( R.style.AdyenCheckout_Bacs_Switch_Account, - localizedContext + localizedContext, ) setAmountConsentSwitchText(bacsDelegate.componentParams) } @@ -172,7 +172,7 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( } private fun outputDataChanged(bacsDirectDebitOutputData: BacsDirectDebitOutputData) { - Logger.v(TAG, "bacsDirectDebitOutputData changed") + adyenLog(AdyenLogLevel.VERBOSE) { "bacsDirectDebitOutputData changed" } onBankAccountNumberValidated(bacsDirectDebitOutputData.bankAccountNumberState) onSortCodeValidated(bacsDirectDebitOutputData.sortCodeState) @@ -206,7 +206,7 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( binding.textInputLayoutBankAccountNumber.hideError() } else if (bankAccountNumberValidation is Validation.Invalid) { binding.textInputLayoutBankAccountNumber.showError( - localizedContext.getString(bankAccountNumberValidation.reason) + localizedContext.getString(bankAccountNumberValidation.reason), ) } } @@ -261,14 +261,14 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( if (amount != null) { val formattedAmount = CurrencyUtils.formatAmount( amount, - componentParams.shopperLocale + componentParams.shopperLocale, ) binding.switchConsentAmount.text = localizedContext.getString(R.string.bacs_consent_amount_specified, formattedAmount) } else { binding.switchConsentAmount.setLocalizedTextFromStyle( R.style.AdyenCheckout_Bacs_Switch_Amount, - localizedContext + localizedContext, ) } } @@ -304,5 +304,3 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor( override fun getView(): View = this } - -private val TAG = LogUtil.getTag() diff --git a/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitComponentTest.kt b/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitComponentTest.kt index b886ca5bca..7b11b35d61 100644 --- a/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitComponentTest.kt +++ b/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitComponentTest.kt @@ -17,8 +17,7 @@ import com.adyen.checkout.bacs.internal.ui.BacsComponentViewType import com.adyen.checkout.bacs.internal.ui.BacsDirectDebitDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -40,7 +39,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class BacsDirectDebitComponentTest( @Mock private val bacsDirectDebitDelegate: BacsDirectDebitDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -59,9 +58,8 @@ internal class BacsDirectDebitComponentTest( bacsDirectDebitDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -115,7 +113,7 @@ internal class BacsDirectDebitComponentTest( bacsDirectDebitDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) component.viewFlow.test { @@ -136,7 +134,7 @@ internal class BacsDirectDebitComponentTest( bacsDirectDebitDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) component.viewFlow.test { diff --git a/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitConfigurationTest.kt b/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitConfigurationTest.kt new file mode 100644 index 0000000000..6cfc6d7ea0 --- /dev/null +++ b/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.bacs + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class BacsDirectDebitConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + bacsDirectDebit { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getBacsDirectDebitConfiguration() + + val expected = BacsDirectDebitConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = BacsDirectDebitConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualBacsConfig = actual.getBacsDirectDebitConfiguration() + assertEquals(config.shopperLocale, actualBacsConfig?.shopperLocale) + assertEquals(config.environment, actualBacsConfig?.environment) + assertEquals(config.clientKey, actualBacsConfig?.clientKey) + assertEquals(config.amount, actualBacsConfig?.amount) + assertEquals(config.analyticsConfiguration, actualBacsConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualBacsConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/bacs/src/test/java/com/adyen/checkout/bacs/internal/DefaultBacsDirectDebitDelegateTest.kt b/bacs/src/test/java/com/adyen/checkout/bacs/internal/DefaultBacsDirectDebitDelegateTest.kt index e463c42620..92bc7cf4ba 100644 --- a/bacs/src/test/java/com/adyen/checkout/bacs/internal/DefaultBacsDirectDebitDelegateTest.kt +++ b/bacs/src/test/java/com/adyen/checkout/bacs/internal/DefaultBacsDirectDebitDelegateTest.kt @@ -13,20 +13,23 @@ import com.adyen.checkout.bacs.BacsDirectDebitComponentState import com.adyen.checkout.bacs.BacsDirectDebitConfiguration import com.adyen.checkout.bacs.BacsDirectDebitMode import com.adyen.checkout.bacs.R +import com.adyen.checkout.bacs.bacsDirectDebit +import com.adyen.checkout.bacs.getBacsDirectDebitConfiguration import com.adyen.checkout.bacs.internal.ui.BacsComponentViewType import com.adyen.checkout.bacs.internal.ui.DefaultBacsDirectDebitDelegate import com.adyen.checkout.bacs.internal.ui.model.BacsDirectDebitOutputData import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -51,7 +54,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultBacsDirectDebitDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, @Mock private val submitHandler: SubmitHandler, @@ -62,7 +65,6 @@ internal class DefaultBacsDirectDebitDelegateTest( @BeforeEach fun beforeEach() { delegate = createBacsDelegate() - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -365,14 +367,14 @@ internal class DefaultBacsDirectDebitDelegateTest( holderNameState = FieldState("test", Validation.Invalid(R.string.bacs_holder_name_invalid)), bankAccountNumberState = FieldState( "12345678", - Validation.Invalid(R.string.bacs_account_number_invalid) + Validation.Invalid(R.string.bacs_account_number_invalid), ), sortCodeState = FieldState("123456", Validation.Invalid(R.string.bacs_sort_code_invalid)), shopperEmailState = FieldState("test@adyen.com", Validation.Valid), isAmountConsentChecked = true, isAccountConsentChecked = false, mode = BacsDirectDebitMode.CONFIRMATION, - ) + ), ) with(expectMostRecentItem()) { @@ -394,7 +396,7 @@ internal class DefaultBacsDirectDebitDelegateTest( isAmountConsentChecked = true, isAccountConsentChecked = true, mode = BacsDirectDebitMode.INPUT, - ) + ), ) with(expectMostRecentItem()) { @@ -416,7 +418,7 @@ internal class DefaultBacsDirectDebitDelegateTest( isAmountConsentChecked = true, isAccountConsentChecked = true, mode = BacsDirectDebitMode.CONFIRMATION, - ) + ), ) with(expectMostRecentItem()) { @@ -434,9 +436,7 @@ internal class DefaultBacsDirectDebitDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultBacsConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createBacsDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -468,9 +468,9 @@ internal class DefaultBacsDirectDebitDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createBacsDelegate( - configuration = getDefaultBacsConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -479,9 +479,9 @@ internal class DefaultBacsDirectDebitDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createBacsDelegate( - configuration = getDefaultBacsConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -583,22 +583,34 @@ internal class DefaultBacsDirectDebitDelegateTest( } private fun createBacsDelegate( - configuration: BacsDirectDebitConfiguration = getDefaultBacsConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: OrderRequest? = TEST_ORDER, ) = DefaultBacsDirectDebitDelegate( observerRepository = PaymentObserverRepository(), - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getBacsDirectDebitConfiguration(), + ), paymentMethod = PaymentMethod(), order = order, analyticsRepository = analyticsRepository, - submitHandler = submitHandler + submitHandler = submitHandler, ) - private fun getDefaultBacsConfigurationBuilder() = BacsDirectDebitConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: BacsDirectDebitConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + bacsDirectDebit(configuration) + } companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.kt index 6c0a5e1c9c..42236dfa79 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.kt @@ -12,10 +12,13 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -26,7 +29,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class BcmcConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -50,6 +53,22 @@ class BcmcConfiguration private constructor( private var shopperReference: String? = null private var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -57,10 +76,11 @@ class BcmcConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** @@ -92,10 +112,11 @@ class BcmcConfiguration private constructor( /** * Set if the option to store the card for future payments should be shown as an input field. * - * Default is false. + * Default is true. * - * When using `sessions` show store payment field will be ignored and replaced with the value - * sent to `/sessions` call. + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. * * @param showStorePaymentField [Boolean] * @return [BcmcConfiguration.Builder] @@ -150,3 +171,38 @@ class BcmcConfiguration private constructor( } } } + +fun CheckoutConfiguration.bcmc( + configuration: @CheckoutConfigurationMarker BcmcConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = BcmcConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.BCMC, config) + return this +} + +fun CheckoutConfiguration.getBcmcConfiguration(): BcmcConfiguration? { + return getConfiguration(PaymentMethodTypes.BCMC) +} + +internal fun BcmcConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.BCMC, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt index 5729ca54e9..2eced9b09a 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/provider/BcmcComponentProvider.kt @@ -20,10 +20,12 @@ import com.adyen.checkout.bcmc.BcmcComponent import com.adyen.checkout.bcmc.BcmcComponentState import com.adyen.checkout.bcmc.BcmcConfiguration import com.adyen.checkout.bcmc.internal.ui.model.BcmcComponentParamsMapper +import com.adyen.checkout.bcmc.toCheckoutConfiguration import com.adyen.checkout.card.internal.data.api.BinLookupService import com.adyen.checkout.card.internal.data.api.DefaultDetectCardTypeRepository import com.adyen.checkout.card.internal.ui.CardValidationMapper import com.adyen.checkout.card.internal.ui.DefaultCardDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -37,12 +39,13 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepo import com.adyen.checkout.components.core.internal.data.api.DefaultPublicKeyRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyService import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.cse.internal.CardEncryptorFactory import com.adyen.checkout.cse.internal.GenericEncryptorFactory import com.adyen.checkout.sessions.core.CheckoutSession @@ -56,14 +59,15 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponen import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.data.api.AddressService import com.adyen.checkout.ui.core.internal.data.api.DefaultAddressRepository +import com.adyen.checkout.ui.core.internal.ui.DefaultAddressLookupDelegate import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class BcmcComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< BcmcComponent, @@ -78,23 +82,29 @@ constructor( SessionComponentCallback, > { - private val componentParamsMapper = BcmcComponentParamsMapper(overrideComponentParams, overrideSessionParams) - @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: BcmcConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, key: String?, ): BcmcComponent { assertSupported(paymentMethod) + val bcmcFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null, paymentMethod) + val componentParams = BcmcComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = paymentMethod, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val publicKeyService = PublicKeyService(httpClient) val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) @@ -131,10 +141,14 @@ constructor( cardEncryptor = cardEncryptor, genericEncryptor = genericEncryptor, submitHandler = SubmitHandler(savedStateHandle), + addressLookupDelegate = DefaultAddressLookupDelegate( + addressRepository = addressRepository, + shopperLocale = componentParams.shopperLocale, + ), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -146,16 +160,38 @@ constructor( componentEventHandler = DefaultComponentEventHandler(), ) } - return ViewModelProvider( - viewModelStoreOwner, - bcmcFactory, - )[key, BcmcComponent::class.java].also { component -> + + return ViewModelProvider(viewModelStoreOwner, bcmcFactory)[key, BcmcComponent::class.java].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) } } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: BcmcConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String?, + ): BcmcComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -163,18 +199,21 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: BcmcConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? ): BcmcComponent { assertSupported(paymentMethod) val bcmcFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - bcmcConfiguration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = BcmcComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), paymentMethod = paymentMethod, ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val publicKeyService = PublicKeyService(httpClient) val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) @@ -212,10 +251,14 @@ constructor( cardEncryptor = cardEncryptor, genericEncryptor = genericEncryptor, submitHandler = SubmitHandler(savedStateHandle), + addressLookupDelegate = DefaultAddressLookupDelegate( + addressRepository = addressRepository, + shopperLocale = componentParams.shopperLocale, + ), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -257,6 +300,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: BcmcConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): BcmcComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt index 71b9c516a7..464b39777a 100644 --- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt +++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapper.kt @@ -9,6 +9,7 @@ package com.adyen.checkout.bcmc.internal.ui.model import com.adyen.checkout.bcmc.BcmcConfiguration +import com.adyen.checkout.bcmc.getBcmcConfiguration import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.CardType import com.adyen.checkout.card.KCPAuthVisibility @@ -16,81 +17,79 @@ import com.adyen.checkout.card.SocialSecurityNumberVisibility import com.adyen.checkout.card.internal.ui.model.CVCVisibility import com.adyen.checkout.card.internal.ui.model.CardComponentParams import com.adyen.checkout.card.internal.ui.model.StoredCVCVisibility +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.ui.core.internal.ui.model.AddressParams +import java.util.Locale internal class BcmcComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - bcmcConfiguration: BcmcConfiguration, - sessionParams: SessionParams?, - paymentMethod: PaymentMethod + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + paymentMethod: PaymentMethod, ): CardComponentParams { - return bcmcConfiguration - .mapToParamsInternal( - supportedCardBrands = paymentMethod.brands?.map { CardBrand(it) } - ) - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + val bcmcConfiguration = checkoutConfiguration.getBcmcConfiguration() + return mapToParams( + commonComponentParamsMapperData.commonComponentParams, + commonComponentParamsMapperData.sessionParams, + bcmcConfiguration, + paymentMethod, + ) } - private fun BcmcConfiguration.mapToParamsInternal(supportedCardBrands: List?): CardComponentParams { + private fun mapToParams( + commonComponentParams: CommonComponentParams, + sessionParams: SessionParams?, + bcmcConfiguration: BcmcConfiguration?, + paymentMethod: PaymentMethod, + ): CardComponentParams { return CardComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - isSubmitButtonVisible = isSubmitButtonVisible ?: true, - isHolderNameRequired = isHolderNameRequired ?: false, - shopperReference = shopperReference, - isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: false, + commonComponentParams = commonComponentParams, + isSubmitButtonVisible = bcmcConfiguration?.isSubmitButtonVisible ?: true, + isHolderNameRequired = bcmcConfiguration?.isHolderNameRequired ?: false, + shopperReference = bcmcConfiguration?.shopperReference, + isStorePaymentFieldVisible = getStorePaymentFieldVisible(sessionParams, bcmcConfiguration), addressParams = AddressParams.None, installmentParams = null, kcpAuthVisibility = KCPAuthVisibility.HIDE, socialSecurityNumberVisibility = SocialSecurityNumberVisibility.HIDE, cvcVisibility = CVCVisibility.HIDE_FIRST, storedCVCVisibility = StoredCVCVisibility.HIDE, - supportedCardBrands = supportedCardBrands ?: DEFAULT_SUPPORTED_CARD_BRANDS + supportedCardBrands = getSupportedCardBrands(paymentMethod), ) } - private fun CardComponentParams.override( - overrideComponentParams: ComponentParams? - ): CardComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) + private fun getStorePaymentFieldVisible( + sessionParams: SessionParams?, + bcmcConfiguration: BcmcConfiguration?, + ): Boolean { + return sessionParams?.enableStoreDetails ?: bcmcConfiguration?.isStorePaymentFieldVisible ?: false } - private fun CardComponentParams.override( - sessionParams: SessionParams? = null - ): CardComponentParams { - if (sessionParams == null) return this - return copy( - isStorePaymentFieldVisible = sessionParams.enableStoreDetails ?: isStorePaymentFieldVisible, - amount = sessionParams.amount ?: amount, - ) + private fun getSupportedCardBrands(paymentMethod: PaymentMethod): List { + return paymentMethod.brands?.map { CardBrand(it) } ?: DEFAULT_SUPPORTED_CARD_BRANDS } companion object { private val DEFAULT_SUPPORTED_CARD_BRANDS = listOf( CardBrand(cardType = CardType.BCMC), CardBrand(cardType = CardType.MAESTRO), - CardBrand(cardType = CardType.VISA) + CardBrand(cardType = CardType.VISA), ) } } diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt index e28059bd49..cb36116969 100644 --- a/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt +++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt @@ -18,12 +18,10 @@ import com.adyen.checkout.card.CardComponentState import com.adyen.checkout.card.internal.ui.CardDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -40,8 +38,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class BcmcComponentTest( @Mock private val cardDelegate: CardDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -62,7 +59,6 @@ internal class BcmcComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -116,7 +112,7 @@ internal class BcmcComponentTest( cardDelegate = cardDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) component.viewFlow.test { @@ -137,7 +133,7 @@ internal class BcmcComponentTest( cardDelegate = cardDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) component.viewFlow.test { diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcConfigurationTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcConfigurationTest.kt new file mode 100644 index 0000000000..004bfed52e --- /dev/null +++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcConfigurationTest.kt @@ -0,0 +1,103 @@ +package com.adyen.checkout.bcmc + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class BcmcConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + bcmc { + setHolderNameRequired(true) + setShowStorePaymentField(true) + setShopperReference("shopperReference") + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getBcmcConfiguration() + + val expected = BcmcConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setHolderNameRequired(true) + .setShowStorePaymentField(true) + .setShopperReference("shopperReference") + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isHolderNameRequired, actual?.isHolderNameRequired) + assertEquals(expected.isStorePaymentFieldVisible, actual?.isStorePaymentFieldVisible) + assertEquals(expected.shopperReference, actual?.shopperReference) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = BcmcConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setHolderNameRequired(true) + .setShowStorePaymentField(true) + .setShopperReference("shopperReference") + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualBcmcConfig = actual.getBcmcConfiguration() + assertEquals(config.shopperLocale, actualBcmcConfig?.shopperLocale) + assertEquals(config.environment, actualBcmcConfig?.environment) + assertEquals(config.clientKey, actualBcmcConfig?.clientKey) + assertEquals(config.amount, actualBcmcConfig?.amount) + assertEquals(config.analyticsConfiguration, actualBcmcConfig?.analyticsConfiguration) + assertEquals(config.isHolderNameRequired, actualBcmcConfig?.isHolderNameRequired) + assertEquals(config.isStorePaymentFieldVisible, actualBcmcConfig?.isStorePaymentFieldVisible) + assertEquals(config.shopperReference, actualBcmcConfig?.shopperReference) + assertEquals(config.isSubmitButtonVisible, actualBcmcConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt index 132f5e05f2..bc11a8b73f 100644 --- a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt +++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/model/BcmcComponentParamsMapperTest.kt @@ -9,6 +9,7 @@ package com.adyen.checkout.bcmc.internal.ui.model import com.adyen.checkout.bcmc.BcmcConfiguration +import com.adyen.checkout.bcmc.bcmc import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.CardType import com.adyen.checkout.card.KCPAuthVisibility @@ -17,10 +18,16 @@ import com.adyen.checkout.card.internal.ui.model.CVCVisibility import com.adyen.checkout.card.internal.ui.model.CardComponentParams import com.adyen.checkout.card.internal.ui.model.StoredCVCVisibility import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams @@ -33,13 +40,13 @@ import java.util.Locale internal class BcmcComponentParamsMapperTest { + private val bcmcComponentParamsMapper = BcmcComponentParamsMapper(CommonComponentParamsMapper()) + @Test - fun `when parent configuration is null and custom bcmc configuration fields are null then all fields should match`() { - val bcmcConfiguration = getBcmcConfigurationBuilder() - .build() + fun `when drop-in override params are null and custom bcmc configuration fields are null then all fields should match`() { + val configuration = createCheckoutConfiguration() - val params = BcmcComponentParamsMapper(null, null) - .mapToParams(bcmcConfiguration, null, PaymentMethod()) + val params = bcmcComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, PaymentMethod()) val expected = getCardComponentParams() @@ -47,51 +54,55 @@ internal class BcmcComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom bcmc configuration fields are set then all fields should match`() { + fun `when drop-in override params are null and custom bcmc configuration fields are set then all fields should match`() { val shopperReference = "SHOPPER_REFERENCE_1" - val bcmcConfiguration = getBcmcConfigurationBuilder() - .setShopperReference(shopperReference) - .setHolderNameRequired(true) - .setShowStorePaymentField(true) - .setSubmitButtonVisible(false) - .build() + val configuration = createCheckoutConfiguration { + setShopperReference(shopperReference) + setHolderNameRequired(true) + setShowStorePaymentField(true) + setSubmitButtonVisible(false) + } - val params = BcmcComponentParamsMapper(null, null) - .mapToParams(bcmcConfiguration, null, PaymentMethod()) + val params = bcmcComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, PaymentMethod()) val expected = getCardComponentParams( isHolderNameRequired = true, shopperReference = shopperReference, isStorePaymentFieldVisible = true, isSubmitButtonVisible = false, - cvcVisibility = CVCVisibility.HIDE_FIRST + cvcVisibility = CVCVisibility.HIDE_FIRST, ) assertEquals(expected, params) } @Test - fun `when parent configuration is set then parent configuration fields should override bcmc configuration fields`() { - val bcmcConfiguration = getBcmcConfigurationBuilder() - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override bcmc configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "USD", - value = 25_00L - ) - ) + value = 25_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + bcmc { + setAmount(Amount("EUR", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } - val params = BcmcComponentParamsMapper(overrideParams, null) - .mapToParams(bcmcConfiguration, null, PaymentMethod()) + val dropInOverrideParams = DropInOverrideParams(Amount("CAD", 123L), null) + val params = bcmcComponentParamsMapper.mapToParams( + configuration, + DEVICE_LOCALE, + dropInOverrideParams, + null, + PaymentMethod(), + ) val expected = getCardComponentParams( shopperLocale = Locale.GERMAN, @@ -100,8 +111,8 @@ internal class BcmcComponentParamsMapperTest { analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "USD", - value = 25_00L + currency = "CAD", + value = 123L, ), ) @@ -116,20 +127,15 @@ internal class BcmcComponentParamsMapperTest { sessionsValue: Boolean?, expectedValue: Boolean ) { - val bcmcConfiguration = getBcmcConfigurationBuilder() - .setShowStorePaymentField(configurationValue) - .build() - - val params = BcmcComponentParamsMapper(null, null).mapToParams( - bcmcConfiguration = bcmcConfiguration, - sessionParams = SessionParams( - enableStoreDetails = sessionsValue, - installmentConfiguration = null, - amount = null, - returnUrl = "", - ), - PaymentMethod() + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(configurationValue) + } + + val sessionParams = createSessionParams( + enableStoreDetails = sessionsValue, ) + val params = + bcmcComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, sessionParams, PaymentMethod()) val expected = getCardComponentParams(isStorePaymentFieldVisible = expectedValue) @@ -138,47 +144,126 @@ internal class BcmcComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val bcmcConfiguration = getBcmcConfigurationBuilder() - .setAmount(configurationValue) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getCardComponentParams(amount = it) } - - val params = BcmcComponentParamsMapper(overrideParams, null).mapToParams( - bcmcConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ), - PaymentMethod() + val configuration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + val params = bcmcComponentParamsMapper.mapToParams( + configuration, + DEVICE_LOCALE, + dropInOverrideParams, + sessionParams, + PaymentMethod(), + ) + + val expected = getCardComponentParams( + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = bcmcComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( - amount = expectedValue + shopperLocale = expectedValue, ) assertEquals(expected, params) } - private fun getBcmcConfigurationBuilder() = BcmcConfiguration.Builder( - shopperLocale = Locale.US, + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = bcmcComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), + ) + + val expected = getCardComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: BcmcConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + bcmc(configuration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, ) @Suppress("LongParameterList") private fun getCardComponentParams( - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -190,12 +275,14 @@ internal class BcmcComponentParamsMapperTest { isStorePaymentFieldVisible: Boolean = false, cvcVisibility: CVCVisibility = CVCVisibility.HIDE_FIRST, ) = CardComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, - amount = amount, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), isSubmitButtonVisible = isSubmitButtonVisible, isHolderNameRequired = isHolderNameRequired, shopperReference = shopperReference, @@ -209,13 +296,14 @@ internal class BcmcComponentParamsMapperTest { supportedCardBrands = listOf( CardBrand(cardType = CardType.BCMC), CardBrand(cardType = CardType.MAESTRO), - CardBrand(cardType = CardType.VISA) - ) + CardBrand(cardType = CardType.VISA), + ), ) companion object { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun enableStoreDetailsSource() = listOf( @@ -235,5 +323,14 @@ internal class BcmcComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/blik/src/main/java/com/adyen/checkout/blik/BlikComponent.kt b/blik/src/main/java/com/adyen/checkout/blik/BlikComponent.kt index d0523b0d15..b400f42a31 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/BlikComponent.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/BlikComponent.kt @@ -22,8 +22,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent @@ -74,25 +74,24 @@ class BlikComponent internal constructor( override fun isConfirmationRequired(): Boolean = blikDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? BlikDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } blikDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() - @JvmField val PROVIDER = BlikComponentProvider() diff --git a/blik/src/main/java/com/adyen/checkout/blik/BlikConfiguration.kt b/blik/src/main/java/com/adyen/checkout/blik/BlikConfiguration.kt index e3c42ddbb2..6ee7c9578e 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/BlikConfiguration.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/BlikConfiguration.kt @@ -12,9 +12,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -25,7 +28,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class BlikConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -43,6 +46,22 @@ class BlikConfiguration private constructor( private var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -50,14 +69,15 @@ class BlikConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -66,7 +86,7 @@ class BlikConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -94,3 +114,38 @@ class BlikConfiguration private constructor( } } } + +fun CheckoutConfiguration.blik( + configuration: @CheckoutConfigurationMarker BlikConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = BlikConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.BLIK, config) + return this +} + +fun CheckoutConfiguration.getBlikConfiguration(): BlikConfiguration? { + return getConfiguration(PaymentMethodTypes.BLIK) +} + +internal fun BlikConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.BLIK, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/blik/src/main/java/com/adyen/checkout/blik/internal/provider/BlikComponentProvider.kt b/blik/src/main/java/com/adyen/checkout/blik/internal/provider/BlikComponentProvider.kt index 19930ad2b9..b0e7cc1aa8 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/internal/provider/BlikComponentProvider.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/internal/provider/BlikComponentProvider.kt @@ -19,8 +19,11 @@ import com.adyen.checkout.action.core.internal.provider.GenericActionComponentPr import com.adyen.checkout.blik.BlikComponent import com.adyen.checkout.blik.BlikComponentState import com.adyen.checkout.blik.BlikConfiguration +import com.adyen.checkout.blik.getBlikConfiguration import com.adyen.checkout.blik.internal.ui.DefaultBlikDelegate import com.adyen.checkout.blik.internal.ui.StoredBlikDelegate +import com.adyen.checkout.blik.toCheckoutConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -35,12 +38,13 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepo import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.provider.StoredPaymentComponentProvider import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -53,46 +57,45 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionStoredPaymentCo import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler +@Suppress("TooManyFunctions") class BlikComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< BlikComponent, BlikConfiguration, BlikComponentState, - ComponentCallback + ComponentCallback, >, StoredPaymentComponentProvider< BlikComponent, BlikConfiguration, BlikComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< BlikComponent, BlikConfiguration, BlikComponentState, - SessionComponentCallback + SessionComponentCallback, >, SessionStoredPaymentComponentProvider< BlikComponent, BlikConfiguration, BlikComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: BlikConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -101,7 +104,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = checkoutConfiguration.getBlikConfiguration(), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -110,7 +119,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -124,8 +133,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -134,7 +143,7 @@ constructor( blikDelegate = blikDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, blikDelegate), - componentEventHandler = DefaultComponentEventHandler() + componentEventHandler = DefaultComponentEventHandler(), ) } @@ -150,17 +159,47 @@ constructor( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, - storedPaymentMethod: StoredPaymentMethod, + paymentMethod: PaymentMethod, configuration: BlikConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, key: String?, + ): BlikComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String?, ): BlikComponent { assertSupported(storedPaymentMethod) val genericStoredFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = checkoutConfiguration.getBlikConfiguration(), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -169,7 +208,7 @@ constructor( storedPaymentMethod = storedPaymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -183,8 +222,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -205,6 +244,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + configuration: BlikConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String?, + ): BlikComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -212,7 +275,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: BlikConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -220,10 +283,14 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = checkoutConfiguration.getBlikConfiguration(), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -234,7 +301,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -248,8 +315,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -265,7 +332,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( @@ -289,6 +356,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: BlikConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): BlikComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -296,7 +387,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, storedPaymentMethod: StoredPaymentMethod, - configuration: BlikConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -304,10 +395,14 @@ constructor( assertSupported(storedPaymentMethod) val genericStoredFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = checkoutConfiguration.getBlikConfiguration(), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -318,7 +413,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -332,8 +427,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -349,7 +444,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = @@ -374,6 +469,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + configuration: BlikConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): BlikComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegate.kt b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegate.kt index 08b73e6515..c1e640c110 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegate.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegate.kt @@ -22,8 +22,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParams import com.adyen.checkout.components.core.paymentmethod.BlikPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent @@ -74,7 +74,7 @@ internal class DefaultBlikDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -109,7 +109,7 @@ internal class DefaultBlikDelegate( } private fun onInputDataChanged() { - Logger.v(TAG, "onInputDataChanged") + adyenLog(AdyenLogLevel.VERBOSE) { "onInputDataChanged" } val outputData = createOutputData() outputDataChanged(outputData) updateComponentState(outputData) @@ -145,7 +145,7 @@ internal class DefaultBlikDelegate( return BlikComponentState( data = paymentComponentData, isInputValid = outputData.isValid, - isReady = true + isReady = true, ) } @@ -165,8 +165,4 @@ internal class DefaultBlikDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/StoredBlikDelegate.kt b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/StoredBlikDelegate.kt index 9995b8ef0a..20beadb7d8 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/StoredBlikDelegate.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/StoredBlikDelegate.kt @@ -21,8 +21,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParams import com.adyen.checkout.components.core.paymentmethod.BlikPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent @@ -67,7 +67,7 @@ internal class StoredBlikDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -84,7 +84,7 @@ internal class StoredBlikDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -97,7 +97,7 @@ internal class StoredBlikDelegate( } override fun updateInputData(update: BlikInputData.() -> Unit) { - Logger.e(TAG, "updateInputData should not be called in StoredBlikDelegate") + adyenLog(AdyenLogLevel.ERROR) { "updateInputData should not be called in StoredBlikDelegate" } } override fun onSubmit() { @@ -123,7 +123,7 @@ internal class StoredBlikDelegate( return BlikComponentState( data = paymentComponentData, isInputValid = true, - isReady = true + isReady = true, ) } @@ -138,8 +138,4 @@ internal class StoredBlikDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/model/BlikOutputData.kt b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/model/BlikOutputData.kt index 3101117e38..20728305eb 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/model/BlikOutputData.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/model/BlikOutputData.kt @@ -11,8 +11,8 @@ import com.adyen.checkout.blik.R import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.OutputData import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog internal class BlikOutputData(blikCode: String) : OutputData { @@ -24,7 +24,7 @@ internal class BlikOutputData(blikCode: String) : OutputData { try { if (blikCode.isNotEmpty()) blikCode.toInt() } catch (e: NumberFormatException) { - Logger.e(TAG, "Failed to parse blik code to Integer", e) + adyenLog(AdyenLogLevel.ERROR, e) { "Failed to parse blik code to Integer" } return Validation.Invalid(R.string.checkout_blik_code_not_valid) } return if (blikCode.length == BLIK_CODE_LENGTH) { @@ -35,7 +35,6 @@ internal class BlikOutputData(blikCode: String) : OutputData { } companion object { - private val TAG = LogUtil.getTag() private const val BLIK_CODE_LENGTH = 6 } diff --git a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/view/BlikView.kt b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/view/BlikView.kt index 0691f4c6e0..de98632798 100644 --- a/blik/src/main/java/com/adyen/checkout/blik/internal/ui/view/BlikView.kt +++ b/blik/src/main/java/com/adyen/checkout/blik/internal/ui/view/BlikView.kt @@ -18,8 +18,8 @@ import com.adyen.checkout.blik.databinding.BlikViewBinding import com.adyen.checkout.blik.internal.ui.BlikDelegate import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.util.hideError import com.adyen.checkout.ui.core.internal.util.setLocalizedHintFromStyle @@ -35,7 +35,7 @@ internal class BlikView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -64,11 +64,11 @@ internal class BlikView @JvmOverloads constructor( private fun initLocalizedStrings(localizedContext: Context) { binding.textInputLayoutBlikCode.setLocalizedHintFromStyle( R.style.AdyenCheckout_Blik_BlikCodeInput, - localizedContext + localizedContext, ) binding.textViewBlikHeader.setLocalizedTextFromStyle( R.style.AdyenCheckout_Blik_BlikHeaderTextView, - localizedContext + localizedContext, ) } @@ -93,7 +93,7 @@ internal class BlikView @JvmOverloads constructor( } override fun highlightValidationErrors() { - Logger.d(TAG, "highlightValidationErrors") + adyenLog(AdyenLogLevel.DEBUG) { "highlightValidationErrors" } val blikCodeValidation = blikDelegate.outputData.blikCodeField.validation if (!blikCodeValidation.isValid()) { binding.textInputLayoutBlikCode.requestFocus() @@ -103,8 +103,4 @@ internal class BlikView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/blik/src/test/java/com/adyen/checkout/blik/BlikComponentTest.kt b/blik/src/test/java/com/adyen/checkout/blik/BlikComponentTest.kt index a83887ea0b..f543545f98 100644 --- a/blik/src/test/java/com/adyen/checkout/blik/BlikComponentTest.kt +++ b/blik/src/test/java/com/adyen/checkout/blik/BlikComponentTest.kt @@ -17,8 +17,7 @@ import com.adyen.checkout.blik.internal.ui.BlikComponentViewType import com.adyen.checkout.blik.internal.ui.BlikDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -40,7 +39,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class BlikComponentTest( @Mock private val blikDelegate: BlikDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -61,7 +60,6 @@ internal class BlikComponentTest( actionHandlingComponent = actionHandlingComponent, componentEventHandler = componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/blik/src/test/java/com/adyen/checkout/blik/BlikConfigurationTest.kt b/blik/src/test/java/com/adyen/checkout/blik/BlikConfigurationTest.kt new file mode 100644 index 0000000000..231438cb92 --- /dev/null +++ b/blik/src/test/java/com/adyen/checkout/blik/BlikConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.blik + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class BlikConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + blik { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getBlikConfiguration() + + val expected = BlikConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = BlikConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualBlikConfig = actual.getBlikConfiguration() + assertEquals(config.shopperLocale, actualBlikConfig?.shopperLocale) + assertEquals(config.environment, actualBlikConfig?.environment) + assertEquals(config.clientKey, actualBlikConfig?.clientKey) + assertEquals(config.amount, actualBlikConfig?.amount) + assertEquals(config.analyticsConfiguration, actualBlikConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualBlikConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/blik/src/test/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegateTest.kt b/blik/src/test/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegateTest.kt index 51e0155e2d..7dd6b6feba 100644 --- a/blik/src/test/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegateTest.kt +++ b/blik/src/test/java/com/adyen/checkout/blik/internal/ui/DefaultBlikDelegateTest.kt @@ -11,16 +11,19 @@ package com.adyen.checkout.blik.internal.ui import app.cash.turbine.test import com.adyen.checkout.blik.BlikComponentState import com.adyen.checkout.blik.BlikConfiguration +import com.adyen.checkout.blik.blik +import com.adyen.checkout.blik.getBlikConfiguration import com.adyen.checkout.blik.internal.ui.model.BlikOutputData import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.core.AdyenLogger +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,7 +48,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultBlikDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, @Mock private val submitHandler: SubmitHandler, @@ -56,7 +59,6 @@ internal class DefaultBlikDelegateTest( @BeforeEach fun beforeEach() { delegate = createBlikDelegate() - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -177,9 +179,7 @@ internal class DefaultBlikDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultBlikConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createBlikDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -204,9 +204,9 @@ internal class DefaultBlikDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createBlikDelegate( - configuration = getDefaultBlikConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -215,9 +215,9 @@ internal class DefaultBlikDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createBlikDelegate( - configuration = getDefaultBlikConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -271,21 +271,33 @@ internal class DefaultBlikDelegateTest( } private fun createBlikDelegate( - configuration: BlikConfiguration = getDefaultBlikConfigurationBuilder().build() + configuration: CheckoutConfiguration = createCheckoutConfiguration() ) = DefaultBlikDelegate( observerRepository = PaymentObserverRepository(), - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getBlikConfiguration(), + ), paymentMethod = PaymentMethod(), order = TEST_ORDER, analyticsRepository = analyticsRepository, submitHandler = submitHandler, ) - private fun getDefaultBlikConfigurationBuilder() = BlikConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: BlikConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + blik(configuration) + } companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt index fc52a77979..93d9494cc6 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt @@ -23,8 +23,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent @@ -79,24 +79,23 @@ class BoletoComponent internal constructor( override fun submit() { (delegate as? ButtonDelegate)?.onSubmit() - ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? BoletoDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } boletoDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = BoletoComponentProvider() diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt index 2152b819a6..b586a9c61a 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt @@ -13,9 +13,11 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -26,7 +28,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class BoletoConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -45,6 +47,22 @@ class BoletoConfiguration private constructor( private var isSubmitButtonVisible: Boolean? = null private var isEmailVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -52,10 +70,11 @@ class BoletoConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** @@ -68,7 +87,7 @@ class BoletoConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -102,7 +121,50 @@ class BoletoConfiguration private constructor( amount = amount, isSubmitButtonVisible = isSubmitButtonVisible, genericActionConfiguration = genericActionConfigurationBuilder.build(), - isEmailVisible = isEmailVisible + isEmailVisible = isEmailVisible, ) } } + +fun CheckoutConfiguration.boleto( + configuration: @CheckoutConfigurationMarker BoletoConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = BoletoConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + + BoletoComponent.PAYMENT_METHOD_TYPES.forEach { key -> + addConfiguration(key, config) + } + + return this +} + +fun CheckoutConfiguration.getBoletoConfiguration(): BoletoConfiguration? { + return BoletoComponent.PAYMENT_METHOD_TYPES.firstNotNullOfOrNull { key -> + getConfiguration(key) + } +} + +internal fun BoletoConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + BoletoComponent.PAYMENT_METHOD_TYPES.forEach { key -> + addConfiguration(key, this@toCheckoutConfiguration) + } + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/provider/BoletoComponentProvider.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/provider/BoletoComponentProvider.kt index 87b9d08d80..0c2fc1aa39 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/internal/provider/BoletoComponentProvider.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/provider/BoletoComponentProvider.kt @@ -21,6 +21,8 @@ import com.adyen.checkout.boleto.BoletoComponentState import com.adyen.checkout.boleto.BoletoConfiguration import com.adyen.checkout.boleto.internal.ui.DefaultBoletoDelegate import com.adyen.checkout.boleto.internal.ui.model.BoletoComponentParamsMapper +import com.adyen.checkout.boleto.toCheckoutConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -32,12 +34,13 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryD import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -54,31 +57,29 @@ import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class BoletoComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< BoletoComponent, BoletoConfiguration, BoletoComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< BoletoComponent, BoletoConfiguration, BoletoComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = BoletoComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: BoletoConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -87,7 +88,13 @@ constructor( assertSupported(paymentMethod) val boletoFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = BoletoComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -97,7 +104,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -112,11 +119,11 @@ constructor( paymentMethod = paymentMethod, order = order, componentParams = componentParams, - addressRepository = addressRepository + addressRepository = addressRepository, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -137,6 +144,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: BoletoConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): BoletoComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -144,7 +175,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: BoletoConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -152,10 +183,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = BoletoComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -166,7 +200,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -180,11 +214,11 @@ constructor( paymentMethod = paymentMethod, order = checkoutSession.order, componentParams = componentParams, - addressRepository = addressRepository + addressRepository = addressRepository, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -200,7 +234,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = @@ -225,6 +259,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: BoletoConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): BoletoComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt index 4fc2d4355b..97d0b10dca 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt @@ -23,9 +23,10 @@ import com.adyen.checkout.components.core.ShopperName import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.paymentmethod.GenericPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.data.api.AddressRepository import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType @@ -33,7 +34,6 @@ import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIState import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.ui.model.AddressParams @@ -104,7 +104,7 @@ internal class DefaultBoletoDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -114,7 +114,7 @@ internal class DefaultBoletoDelegate( addressRepository.statesFlow .distinctUntilChanged() .onEach { states -> - Logger.d(TAG, "New states emitted - states: ${states.size}") + adyenLog(AdyenLogLevel.DEBUG) { "New states emitted - states: ${states.size}" } updateOutputData(stateOptions = AddressFormUtils.initializeStateOptions(states)) } .launchIn(coroutineScope) @@ -127,7 +127,7 @@ internal class DefaultBoletoDelegate( val countryOptions = AddressFormUtils.initializeCountryOptions( shopperLocale = componentParams.shopperLocale, addressParams = componentParams.addressParams, - countryList = countries + countryList = countries, ) countryOptions.firstOrNull { it.selected }?.let { inputData.address.country = it.code @@ -141,7 +141,7 @@ internal class DefaultBoletoDelegate( private fun requestCountryList() { addressRepository.getCountryList( shopperLocale = componentParams.shopperLocale, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, ) } @@ -149,7 +149,7 @@ internal class DefaultBoletoDelegate( addressRepository.getStateList( shopperLocale = componentParams.shopperLocale, countryCode = countryCode, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, ) } @@ -167,7 +167,7 @@ internal class DefaultBoletoDelegate( private fun onInputDataChanged() { val outputData = createOutputData( countryOptions = outputData.addressState.countryOptions, - stateOptions = outputData.addressState.stateOptions + stateOptions = outputData.addressState.stateOptions, ) _outputDataFlow.tryEmit(outputData) updateComponentState(outputData) @@ -189,11 +189,11 @@ internal class DefaultBoletoDelegate( ): BoletoOutputData { val updatedCountryOptions = AddressFormUtils.markAddressListItemSelected( countryOptions, - inputData.address.country + inputData.address.country, ) val updatedStateOptions = AddressFormUtils.markAddressListItemSelected( stateOptions, - inputData.address.stateOrProvince + inputData.address.stateOrProvince, ) val addressFormUIState = AddressFormUIState.fromAddressParams(componentParams.addressParams) @@ -202,28 +202,28 @@ internal class DefaultBoletoDelegate( firstNameState = BoletoValidationUtils.validateFirstName(inputData.firstName), lastNameState = BoletoValidationUtils.validateLastName(inputData.lastName), socialSecurityNumberState = SocialSecurityNumberUtils.validateSocialSecurityNumber( - inputData.socialSecurityNumber + inputData.socialSecurityNumber, ), addressState = AddressValidationUtils.validateAddressInput( inputData.address, addressFormUIState, updatedCountryOptions, updatedStateOptions, - false + false, ), addressUIState = addressFormUIState, isEmailVisible = componentParams.isEmailVisible, isSendEmailSelected = inputData.isSendEmailSelected, shopperEmailState = BoletoValidationUtils.validateShopperEmail( inputData.isSendEmailSelected, - inputData.shopperEmail - ) + inputData.shopperEmail, + ), ) } @VisibleForTesting internal fun updateComponentState(outputData: BoletoOutputData) { - Logger.v(TAG, "updateComponentState") + adyenLog(AdyenLogLevel.VERBOSE) { "updateComponentState" } val componentState = createComponentState(outputData) _componentStateFlow.tryEmit(componentState) } @@ -241,8 +241,8 @@ internal class DefaultBoletoDelegate( socialSecurityNumber = outputData.socialSecurityNumberState.value, shopperName = ShopperName( firstName = outputData.firstNameState.value, - lastName = outputData.lastNameState.value - ) + lastName = outputData.lastNameState.value, + ), ) if (outputData.isSendEmailSelected) { paymentComponentData.shopperEmail = outputData.shopperEmailState.value @@ -250,7 +250,7 @@ internal class DefaultBoletoDelegate( if (AddressFormUtils.isAddressRequired(outputData.addressUIState)) { paymentComponentData.billingAddress = AddressFormUtils.makeAddressData( addressOutputData = outputData.addressState, - addressFormUIState = outputData.addressUIState + addressFormUIState = outputData.addressUIState, ) } val countriesList: List = outputData.addressState.countryOptions @@ -259,7 +259,7 @@ internal class DefaultBoletoDelegate( return BoletoComponentState( data = paymentComponentData, isInputValid = outputData.isValid, - isReady = countriesList.isNotEmpty() && statesList.isNotEmpty() + isReady = countriesList.isNotEmpty() && statesList.isNotEmpty(), ) } @@ -282,7 +282,7 @@ internal class DefaultBoletoDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -301,8 +301,4 @@ internal class DefaultBoletoDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParams.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParams.kt index bf9f3ea206..b9441922d9 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParams.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParams.kt @@ -8,22 +8,14 @@ package com.adyen.checkout.boleto.internal.ui.model -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams -import java.util.Locale internal data class BoletoComponentParams( + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, val addressParams: AddressParams, val isEmailVisible: Boolean, -) : ComponentParams, ButtonParams +) : ComponentParams by commonComponentParams, ButtonParams diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapper.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapper.kt index 79b7111617..26bf1bffcc 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapper.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapper.kt @@ -8,65 +8,42 @@ package com.adyen.checkout.boleto.internal.ui.model -import com.adyen.checkout.boleto.BoletoConfiguration -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.boleto.getBoletoConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.ui.core.internal.ui.model.AddressParams +import java.util.Locale internal class BoletoComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - configuration: BoletoConfiguration, - sessionParams: SessionParams? + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, ): BoletoComponentParams { - return configuration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + val boletoConfiguration = checkoutConfiguration.getBoletoConfiguration() + val commonComponentParams = commonComponentParamsMapperData.commonComponentParams - private fun BoletoConfiguration.mapToParamsInternal(): BoletoComponentParams { return BoletoComponentParams( - isSubmitButtonVisible = isSubmitButtonVisible ?: true, - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, + commonComponentParams = commonComponentParams, + isSubmitButtonVisible = boletoConfiguration?.isSubmitButtonVisible ?: true, addressParams = AddressParams.FullAddress( defaultCountryCode = BRAZIL_COUNTRY_CODE, supportedCountryCodes = DEFAULT_SUPPORTED_COUNTRY_LIST, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ), - isEmailVisible = isEmailVisible ?: false - ) - } - - private fun BoletoComponentParams.override( - overrideComponentParams: ComponentParams? - ): BoletoComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) - } - - private fun BoletoComponentParams.override( - sessionParams: SessionParams? - ): BoletoComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, + isEmailVisible = boletoConfiguration?.isEmailVisible ?: false, ) } diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoInputData.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoInputData.kt index 30c5c82260..ac72c68079 100644 --- a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoInputData.kt +++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoInputData.kt @@ -8,8 +8,8 @@ package com.adyen.checkout.boleto.internal.ui.model +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.InputData -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel internal data class BoletoInputData( var firstName: String = "", diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt index 8ec78b159c..f06916df0c 100644 --- a/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt +++ b/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt @@ -16,8 +16,7 @@ import com.adyen.checkout.boleto.internal.ui.BoletoComponentViewType import com.adyen.checkout.boleto.internal.ui.BoletoDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.test.extensions.test @@ -42,7 +41,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class BoletoComponentTest( @Mock private val boletoDelegate: BoletoDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -62,8 +61,6 @@ internal class BoletoComponentTest( actionHandlingComponent = actionHandlingComponent, componentEventHandler = componentEventHandler, ) - - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -115,7 +112,7 @@ internal class BoletoComponentTest( boletoDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) val viewTestFlow = component.viewFlow.test(testScheduler) @@ -135,7 +132,7 @@ internal class BoletoComponentTest( boletoDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) val viewTestFlow = component.viewFlow.test(testScheduler) diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/BoletoConfigurationTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/BoletoConfigurationTest.kt new file mode 100644 index 0000000000..bdae6b0177 --- /dev/null +++ b/boleto/src/test/java/com/adyen/checkout/boleto/BoletoConfigurationTest.kt @@ -0,0 +1,93 @@ +package com.adyen.checkout.boleto + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class BoletoConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + boleto { + setSubmitButtonVisible(false) + setEmailVisibility(true) + } + } + + val actual = checkoutConfiguration.getBoletoConfiguration() + + val expected = BoletoConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .setEmailVisibility(true) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + assertEquals(expected.isEmailVisible, actual?.isEmailVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = BoletoConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .setEmailVisibility(true) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualBoletoConfig = actual.getBoletoConfiguration() + assertEquals(config.shopperLocale, actualBoletoConfig?.shopperLocale) + assertEquals(config.environment, actualBoletoConfig?.environment) + assertEquals(config.clientKey, actualBoletoConfig?.clientKey) + assertEquals(config.amount, actualBoletoConfig?.amount) + assertEquals(config.analyticsConfiguration, actualBoletoConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualBoletoConfig?.isSubmitButtonVisible) + assertEquals(config.isEmailVisible, actualBoletoConfig?.isEmailVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegateTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegateTest.kt index 2e27a7289b..9e4e33f97d 100644 --- a/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegateTest.kt +++ b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegateTest.kt @@ -11,20 +11,22 @@ package com.adyen.checkout.boleto.internal.ui import app.cash.turbine.test import com.adyen.checkout.boleto.BoletoComponentState import com.adyen.checkout.boleto.BoletoConfiguration +import com.adyen.checkout.boleto.boleto import com.adyen.checkout.boleto.internal.ui.model.BoletoComponentParamsMapper import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.core.AdyenLogger +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.extensions.test import com.adyen.checkout.ui.core.internal.test.TestAddressRepository import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel @@ -39,7 +41,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension @@ -49,7 +51,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultBoletoDelegateTest( @Mock private val submitHandler: SubmitHandler, @Mock private val analyticsRepository: AnalyticsRepository, @@ -63,7 +65,6 @@ internal class DefaultBoletoDelegateTest( fun beforeEach() { addressRepository = TestAddressRepository() delegate = createBoletoDelegate() - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -448,9 +449,7 @@ internal class DefaultBoletoDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultBoletoConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createBoletoDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -515,15 +514,16 @@ internal class DefaultBoletoDelegateTest( paymentMethod: PaymentMethod = PaymentMethod(), addressRepository: TestAddressRepository = this.addressRepository, order: Order? = TEST_ORDER, - configuration: BoletoConfiguration = getDefaultBoletoConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), ) = DefaultBoletoDelegate( submitHandler = submitHandler, analyticsRepository = analyticsRepository, observerRepository = PaymentObserverRepository(), paymentMethod = paymentMethod, order = order, - componentParams = BoletoComponentParamsMapper(null, null).mapToParams(configuration, null), - addressRepository = addressRepository + componentParams = BoletoComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), + addressRepository = addressRepository, ) @Suppress("LongParameterList") @@ -542,11 +542,19 @@ internal class DefaultBoletoDelegateTest( houseNumberOrName = houseNumberOrName, apartmentSuite = apartmentSuite, city = city, - country = country + country = country, ) - private fun getDefaultBoletoConfigurationBuilder(): BoletoConfiguration.Builder { - return BoletoConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: BoletoConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + boleto(configuration) } companion object { @@ -558,10 +566,10 @@ internal class DefaultBoletoDelegateTest( @JvmStatic fun amountSource() = listOf( // configurationValue, expectedComponentStateValue - Arguments.arguments(Amount("EUR", 100), Amount("EUR", 100)), - Arguments.arguments(Amount("USD", 0), Amount("USD", 0)), - Arguments.arguments(null, null), - Arguments.arguments(null, null), + arguments(Amount("EUR", 100), Amount("EUR", 100)), + arguments(Amount("USD", 0), Amount("USD", 0)), + arguments(null, null), + arguments(null, null), ) } } diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt index 3c9f14dc66..6e8b1f9795 100644 --- a/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt +++ b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt @@ -9,72 +9,81 @@ package com.adyen.checkout.boleto.internal.ui.model import com.adyen.checkout.boleto.BoletoConfiguration +import com.adyen.checkout.boleto.boleto import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments import org.junit.jupiter.params.provider.MethodSource import java.util.Locale internal class BoletoComponentParamsMapperTest { - @Test - fun `when parent configuration is null and custom boleto configuration fields are null, them all fields should match`() { - val boletoConfiguration = getBoletoConfigurationBuilder().build() + private val boletoComponentParamsMapper = BoletoComponentParamsMapper(CommonComponentParamsMapper()) - val params = getBoletoComponentParamsMapper().mapToParams(boletoConfiguration, null) + @Test + fun `when drop-in override params are null and custom boleto configuration fields are null, them all fields should match`() { + val params = mapParams() val expected = getBoletoComponentParams() assertEquals(expected, params) } @Test - fun `when parent configuration is null and custom fields are set then all fields should match`() { - val boletoConfiguration = getBoletoConfigurationBuilder() - .setEmailVisibility(true) - .build() + fun `when drop-in override params are null and custom fields are set then all fields should match`() { + val configuration = createCheckoutConfiguration { + setEmailVisibility(true) + } - val params = getBoletoComponentParamsMapper().mapToParams(boletoConfiguration, null) + val params = mapParams(configuration) val expectedAddressParams = AddressParams.FullAddress( defaultCountryCode = BRAZIL_COUNTRY_CODE, supportedCountryCodes = SUPPORTED_COUNTRY_LIST_1, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ) val expected = getBoletoComponentParams( addressParams = expectedAddressParams, - isSendEmailVisible = true + isSendEmailVisible = true, ) assertEquals(expected, params) } @Test - fun `when parent configuration is set then parent configuration should override Boleto configuration fields`() { - val boletoConfiguration = getBoletoConfigurationBuilder().build() - - val overrideComponentParams = GenericComponentParams( + fun `when drop-in override params are set then they should override Boleto configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "CAD", - value = 123_00L - ) - ) + value = 123_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + boleto { + setAmount(Amount("USD", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } - val params = getBoletoComponentParamsMapper(overrideComponentParams = overrideComponentParams).mapToParams( - boletoConfiguration, - null + val dropInOverrideParams = DropInOverrideParams(Amount("EUR", 20L), null) + val params = mapParams( + configuration = configuration, + dropInOverrideParams = dropInOverrideParams, ) val expected = getBoletoComponentParams( @@ -84,9 +93,9 @@ internal class BoletoComponentParamsMapperTest { analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "CAD", - value = 123_00L - ) + currency = "EUR", + value = 20L, + ), ) assertEquals(expected, params) @@ -94,13 +103,13 @@ internal class BoletoComponentParamsMapperTest { @Test fun `when send email is set, them params should match`() { - val boletoConfiguration = getBoletoConfigurationBuilder() - .setEmailVisibility(true) - .build() + val configuration = createCheckoutConfiguration { + setEmailVisibility(true) + } - val params = getBoletoComponentParamsMapper().mapToParams(boletoConfiguration, null) + val params = mapParams(configuration) val expected = getBoletoComponentParams( - isSendEmailVisible = true + isSendEmailVisible = true, ) assertEquals(expected, params) @@ -108,47 +117,124 @@ internal class BoletoComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val boletoConfiguration = getBoletoConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val params = mapParams( + configuration = configuration, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + ) + + val expected = getBoletoComponentParams( + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = boletoComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getBoletoComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getBoletoComponentParams(amount = it) } + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) - val params = getBoletoComponentParamsMapper(overrideComponentParams = overrideParams).mapToParams( - boletoConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ) + val params = boletoComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, ) val expected = getBoletoComponentParams( - amount = expectedValue + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, ) assertEquals(expected, params) } - private fun getBoletoConfigurationBuilder() = BoletoConfiguration.Builder( - shopperLocale = Locale.US, + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: BoletoConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + boleto(configuration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, ) @Suppress("LongParameterList") private fun getBoletoComponentParams( isSubmitButtonVisible: Boolean = true, - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -157,38 +243,59 @@ internal class BoletoComponentParamsMapperTest { addressParams: AddressParams = AddressParams.FullAddress( defaultCountryCode = BRAZIL_COUNTRY_CODE, supportedCountryCodes = SUPPORTED_COUNTRY_LIST_1, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ), isSendEmailVisible: Boolean = false ) = BoletoComponentParams( isSubmitButtonVisible = isSubmitButtonVisible, - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, - amount = amount, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), addressParams = addressParams, - isEmailVisible = isSendEmailVisible + isEmailVisible = isSendEmailVisible, ) - private fun getBoletoComponentParamsMapper( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, - ) = BoletoComponentParamsMapper(overrideComponentParams, overrideSessionParams) + private fun mapParams( + configuration: CheckoutConfiguration = createCheckoutConfiguration(), + locale: Locale = DEVICE_LOCALE, + dropInOverrideParams: DropInOverrideParams? = null, + componentSessionParams: SessionParams? = null, + ): BoletoComponentParams { + return boletoComponentParamsMapper.mapToParams( + configuration, + locale, + dropInOverrideParams, + componentSessionParams, + ) + } companion object { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" private const val BRAZIL_COUNTRY_CODE = "BR" private val SUPPORTED_COUNTRY_LIST_1 = listOf(BRAZIL_COUNTRY_CODE) + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun amountSource() = listOf( // configurationValue, dropInValue, sessionsValue, expectedValue - Arguments.arguments(Amount("EUR", 100), Amount("USD", 200), Amount("CAD", 300), Amount("CAD", 300)), - Arguments.arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), - Arguments.arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), + arguments(Amount("EUR", 100), Amount("USD", 200), Amount("CAD", 300), Amount("CAD", 300)), + arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), + arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), + ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), ) } } diff --git a/card/src/main/java/com/adyen/checkout/card/AddressConfiguration.kt b/card/src/main/java/com/adyen/checkout/card/AddressConfiguration.kt index 8fce183347..0d3a4adc7b 100644 --- a/card/src/main/java/com/adyen/checkout/card/AddressConfiguration.kt +++ b/card/src/main/java/com/adyen/checkout/card/AddressConfiguration.kt @@ -37,6 +37,12 @@ sealed class AddressConfiguration : Parcelable { val addressFieldPolicy: CardAddressFieldPolicy = CardAddressFieldPolicy.Required() ) : AddressConfiguration() + /** + * Address Lookup option will be shown as part of card component. + */ + @Parcelize + class Lookup : AddressConfiguration() + /** * Configuration for requirement of the address fields. */ diff --git a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt index 108bc5fa14..d4c6aa6fec 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt @@ -17,15 +17,19 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.card.internal.provider.CardComponentProvider import com.adyen.checkout.card.internal.ui.CardDelegate +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.AddressLookupComponent import com.adyen.checkout.components.core.internal.ButtonComponent import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent @@ -35,6 +39,7 @@ import kotlinx.coroutines.flow.Flow /** * A [PaymentComponent] that supports the [PaymentMethodTypes.SCHEME] payment method. */ +@Suppress("TooManyFunctions") open class CardComponent constructor( private val cardDelegate: CardDelegate, private val genericActionDelegate: GenericActionDelegate, @@ -45,6 +50,7 @@ open class CardComponent constructor( PaymentComponent, ViewableComponent, ButtonComponent, + AddressLookupComponent, ActionHandlingComponent by actionHandlingComponent { override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate @@ -84,12 +90,13 @@ open class CardComponent constructor( override fun isConfirmationRequired(): Boolean = cardDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? CardDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } /** @@ -112,16 +119,31 @@ open class CardComponent constructor( cardDelegate.setOnBinLookupListener(listener) } + override fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) { + cardDelegate.setAddressLookupCallback(addressLookupCallback) + } + + override fun updateAddressLookupOptions(options: List) { + cardDelegate.updateAddressLookupOptions(options) + } + + override fun setAddressLookupResult(addressLookupResult: AddressLookupResult) { + cardDelegate.setAddressLookupResult(addressLookupResult) + } + + fun handleBackPress(): Boolean { + return (delegate as? CardDelegate)?.handleBackPress() ?: false + } + override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } cardDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = CardComponentProvider() diff --git a/card/src/main/java/com/adyen/checkout/card/CardConfiguration.kt b/card/src/main/java/com/adyen/checkout/card/CardConfiguration.kt index c715f79bac..69e6de8a7b 100644 --- a/card/src/main/java/com/adyen/checkout/card/CardConfiguration.kt +++ b/card/src/main/java/com/adyen/checkout/card/CardConfiguration.kt @@ -12,11 +12,14 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -27,7 +30,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class CardConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -65,6 +68,22 @@ class CardConfiguration private constructor( private var installmentConfiguration: InstallmentConfiguration? = null private var addressConfiguration: AddressConfiguration? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -72,10 +91,11 @@ class CardConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** @@ -137,8 +157,9 @@ class CardConfiguration private constructor( * * Default is true. * - * When using `sessions` show store payment field will be ignored and replaced with the value - * sent to `/sessions` call. + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. * * @param showStorePaymentField [Boolean] * @return [CardConfiguration.Builder] @@ -220,8 +241,9 @@ class CardConfiguration private constructor( /** * Configures the installment options to be provided to the shopper. * - * When using `sessions` installment configuration will be ignored and replaced with the value - * sent to `/sessions` call. + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. * * @param installmentConfiguration The configuration object for installment options. * @return [CardConfiguration.Builder] @@ -279,7 +301,7 @@ class CardConfiguration private constructor( kcpAuthVisibility = kcpAuthVisibility, installmentConfiguration = installmentConfiguration, addressConfiguration = addressConfiguration, - genericActionConfiguration = genericActionConfigurationBuilder.build() + genericActionConfiguration = genericActionConfigurationBuilder.build(), ) } } @@ -288,7 +310,42 @@ class CardConfiguration private constructor( val DEFAULT_SUPPORTED_CARDS_LIST: List = listOf( CardBrand(cardType = CardType.VISA), CardBrand(cardType = CardType.AMERICAN_EXPRESS), - CardBrand(cardType = CardType.MASTERCARD) + CardBrand(cardType = CardType.MASTERCARD), ) } } + +fun CheckoutConfiguration.card( + configuration: @CheckoutConfigurationMarker CardConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = CardConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.SCHEME, config) + return this +} + +fun CheckoutConfiguration.getCardConfiguration(): CardConfiguration? { + return getConfiguration(PaymentMethodTypes.SCHEME) +} + +internal fun CardConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.SCHEME, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt index 47affe25db..dbae165ccb 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/api/BinLookupService.kt @@ -13,24 +13,26 @@ import com.adyen.checkout.card.internal.data.model.BinLookupRequest import com.adyen.checkout.card.internal.data.model.BinLookupResponse import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.post +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class BinLookupService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { suspend fun makeBinLookup( request: BinLookupRequest, clientKey: String, - ): BinLookupResponse = withContext(Dispatchers.IO) { + ): BinLookupResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v2/bin/binLookup", queryParameters = mapOf("clientKey" to clientKey), body = request, requestSerializer = BinLookupRequest.SERIALIZER, - responseSerializer = BinLookupResponse.SERIALIZER + responseSerializer = BinLookupResponse.SERIALIZER, ) } } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt b/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt index 9c9426a36f..31f48892bc 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/data/api/DefaultDetectCardTypeRepository.kt @@ -17,9 +17,9 @@ import com.adyen.checkout.card.internal.data.model.BinLookupResult import com.adyen.checkout.card.internal.data.model.Brand import com.adyen.checkout.card.internal.data.model.DetectedCardType import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.internal.util.Sha256 +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching import com.adyen.checkout.cse.internal.BaseCardEncryptor import kotlinx.coroutines.CoroutineScope @@ -49,26 +49,28 @@ class DefaultDetectCardTypeRepository( coroutineScope: CoroutineScope, type: String? ) { - Logger.d(TAG, "detectCardType") + adyenLog(AdyenLogLevel.DEBUG) { "detectCardType" } if (shouldFetchReliableTypes(cardNumber)) { when (val cachedResult = getFromCache(cardNumber)) { is BinLookupResult.Available -> { - Logger.d(TAG, "Retrieving from cache.") + adyenLog(AdyenLogLevel.DEBUG) { "Retrieving from cache." } _detectedCardTypesFlow.trySend(cachedResult.detectedCardTypes) return } + is BinLookupResult.Loading -> { - Logger.d(TAG, "BinLookup request is in progress.") + adyenLog(AdyenLogLevel.DEBUG) { "BinLookup request is in progress." } } + is BinLookupResult.Unavailable -> { - Logger.d(TAG, "Fetching from network.") + adyenLog(AdyenLogLevel.DEBUG) { "Fetching from network." } fetchFromNetwork( cardNumber, publicKey, supportedCardBrands, clientKey, coroutineScope, - type + type, ) } } @@ -86,16 +88,16 @@ class DefaultDetectCardTypeRepository( type: String? ) { if (publicKey != null) { - Logger.d(TAG, "Launching Bin Lookup") + adyenLog(AdyenLogLevel.DEBUG) { "Launching Bin Lookup" } coroutineScope.launch { - Logger.d(TAG, "Emitting new detectedCardTypes") + adyenLog(AdyenLogLevel.DEBUG) { "Emitting new detectedCardTypes" } fetch( cardNumber, publicKey, supportedCardBrands, clientKey, - type + type, )?.let { _detectedCardTypesFlow.send(it) } @@ -104,7 +106,7 @@ class DefaultDetectCardTypeRepository( } private fun detectCardLocally(cardNumber: String, supportedCardBrands: List): List { - Logger.d(TAG, "detectCardLocally") + adyenLog(AdyenLogLevel.DEBUG) { "detectCardLocally" } if (cardNumber.isEmpty()) { return emptyList() } @@ -175,16 +177,16 @@ class DefaultDetectCardTypeRepository( binLookupService.makeBinLookup( request = request, - clientKey = clientKey + clientKey = clientKey, ) } - .onFailure { e -> Logger.e(TAG, "checkCardType - Failed to do bin lookup", e) } + .onFailure { e -> adyenLog(AdyenLogLevel.ERROR, e) { "checkCardType - Failed to do bin lookup" } } .getOrNull() } private fun mapResponse(binLookupResponse: BinLookupResponse): List { - Logger.d(TAG, "handleBinLookupResponse") - Logger.v(TAG, "Brands: ${binLookupResponse.brands}") + adyenLog(AdyenLogLevel.DEBUG) { "handleBinLookupResponse" } + adyenLog(AdyenLogLevel.VERBOSE) { "Brands: ${binLookupResponse.brands}" } // Any null or unmapped values are ignored, a null response becomes an empty list return binLookupResponse.brands.orEmpty().mapNotNull { brandResponse -> @@ -195,10 +197,10 @@ class DefaultDetectCardTypeRepository( isReliable = true, enableLuhnCheck = brandResponse.enableLuhnCheck == true, cvcPolicy = Brand.FieldPolicy.parse( - brandResponse.cvcPolicy ?: Brand.FieldPolicy.REQUIRED.value + brandResponse.cvcPolicy ?: Brand.FieldPolicy.REQUIRED.value, ), expiryDatePolicy = Brand.FieldPolicy.parse( - brandResponse.expiryDatePolicy ?: Brand.FieldPolicy.REQUIRED.value + brandResponse.expiryDatePolicy ?: Brand.FieldPolicy.REQUIRED.value, ), isSupported = brandResponse.supported != false, panLength = brandResponse.panLength, @@ -208,7 +210,6 @@ class DefaultDetectCardTypeRepository( } companion object { - private val TAG = LogUtil.getTag() private val NO_CVC_BRANDS: Set = hashSetOf(CardBrand(cardType = CardType.BCMC)) private const val REQUIRED_BIN_SIZE = 11 diff --git a/card/src/main/java/com/adyen/checkout/card/internal/provider/CardComponentProvider.kt b/card/src/main/java/com/adyen/checkout/card/internal/provider/CardComponentProvider.kt index ced949a235..9a72fc7a80 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/provider/CardComponentProvider.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/provider/CardComponentProvider.kt @@ -25,6 +25,8 @@ import com.adyen.checkout.card.internal.ui.DefaultCardDelegate import com.adyen.checkout.card.internal.ui.StoredCardDelegate import com.adyen.checkout.card.internal.ui.model.CardComponentParamsMapper import com.adyen.checkout.card.internal.ui.model.InstallmentsParamsMapper +import com.adyen.checkout.card.toCheckoutConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -40,12 +42,13 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultPublicKeyRepo import com.adyen.checkout.components.core.internal.data.api.PublicKeyService import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.provider.StoredPaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.cse.internal.CardEncryptorFactory import com.adyen.checkout.cse.internal.GenericEncryptorFactory import com.adyen.checkout.sessions.core.CheckoutSession @@ -60,53 +63,49 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionStoredPaymentCo import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.data.api.AddressService import com.adyen.checkout.ui.core.internal.data.api.DefaultAddressRepository +import com.adyen.checkout.ui.core.internal.ui.DefaultAddressLookupDelegate import com.adyen.checkout.ui.core.internal.ui.SubmitHandler +@Suppress("TooManyFunctions") class CardComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< CardComponent, CardConfiguration, CardComponentState, - ComponentCallback + ComponentCallback, >, StoredPaymentComponentProvider< CardComponent, CardConfiguration, CardComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< CardComponent, CardConfiguration, CardComponentState, - SessionComponentCallback + SessionComponentCallback, >, SessionStoredPaymentComponentProvider< CardComponent, CardConfiguration, CardComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = CardComponentParamsMapper( - installmentsParamsMapper = InstallmentsParamsMapper(), - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams - ) - @Suppress("LongParameterList", "LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: CardConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -115,11 +114,17 @@ constructor( assertSupported(paymentMethod) val factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParamsDefault( - configuration, - paymentMethod, - null, + val componentParams = CardComponentParamsMapper( + CommonComponentParamsMapper(), + InstallmentsParamsMapper(), + ).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = paymentMethod, ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val genericEncryptor = GenericEncryptorFactory.provide() val cardEncryptor = CardEncryptorFactory.provide() @@ -138,7 +143,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -155,11 +160,15 @@ constructor( cardValidationMapper = cardValidationMapper, cardEncryptor = cardEncryptor, genericEncryptor = genericEncryptor, - submitHandler = SubmitHandler(savedStateHandle) + submitHandler = SubmitHandler(savedStateHandle), + addressLookupDelegate = DefaultAddressLookupDelegate( + addressRepository = DefaultAddressRepository(AddressService(httpClient)), + shopperLocale = componentParams.shopperLocale, + ), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -184,21 +193,52 @@ constructor( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, - checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, configuration: CardConfiguration, application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): CardComponent { + return get( + savedStateRegistryOwner, + viewModelStoreOwner, + lifecycleOwner, + paymentMethod, + configuration.toCheckoutConfiguration(), + application, + componentCallback, + order, + key, + ) + } + + @Suppress("LongParameterList", "LongMethod") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, componentCallback: SessionComponentCallback, key: String? ): CardComponent { assertSupported(paymentMethod) val factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParamsDefault( - cardConfiguration = configuration, + val componentParams = CardComponentParamsMapper( + CommonComponentParamsMapper(), + InstallmentsParamsMapper(), + ).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), paymentMethod = paymentMethod, - sessionParams = SessionParamsFactory.create(checkoutSession), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val genericEncryptor = GenericEncryptorFactory.provide() val cardEncryptor = CardEncryptorFactory.provide() @@ -218,7 +258,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -235,11 +275,15 @@ constructor( cardValidationMapper = cardValidationMapper, cardEncryptor = cardEncryptor, genericEncryptor = genericEncryptor, - submitHandler = SubmitHandler(savedStateHandle) + submitHandler = SubmitHandler(savedStateHandle), + addressLookupDelegate = DefaultAddressLookupDelegate( + addressRepository = DefaultAddressRepository(AddressService(httpClient)), + shopperLocale = componentParams.shopperLocale, + ), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -254,7 +298,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -276,13 +320,38 @@ constructor( } } + @Suppress("LongParameterList") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: CardConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): CardComponent { + return get( + savedStateRegistryOwner, + viewModelStoreOwner, + lifecycleOwner, + checkoutSession, + paymentMethod, + configuration.toCheckoutConfiguration(), + application, + componentCallback, + key, + ) + } + @Suppress("LongParameterList", "LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, storedPaymentMethod: StoredPaymentMethod, - configuration: CardConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -291,7 +360,17 @@ constructor( assertSupported(storedPaymentMethod) val factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParamsStored(configuration, null) + val componentParams = CardComponentParamsMapper( + CommonComponentParamsMapper(), + InstallmentsParamsMapper(), + ).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + storedPaymentMethod = storedPaymentMethod, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val publicKeyService = PublicKeyService(httpClient) val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) @@ -304,7 +383,7 @@ constructor( storedPaymentMethod = storedPaymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -317,11 +396,11 @@ constructor( analyticsRepository = analyticsRepository, cardEncryptor = cardEncryptor, publicKeyRepository = publicKeyRepository, - submitHandler = SubmitHandler(savedStateHandle) + submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -341,6 +420,31 @@ constructor( } } + @Suppress("LongParameterList") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + configuration: CardConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): CardComponent { + return get( + savedStateRegistryOwner, + viewModelStoreOwner, + lifecycleOwner, + storedPaymentMethod, + configuration.toCheckoutConfiguration(), + application, + componentCallback, + order, + key, + ) + } + @Suppress("LongParameterList", "LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -348,7 +452,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, storedPaymentMethod: StoredPaymentMethod, - configuration: CardConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -356,10 +460,17 @@ constructor( assertSupported(storedPaymentMethod) val factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParamsStored( - cardConfiguration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = CardComponentParamsMapper( + CommonComponentParamsMapper(), + InstallmentsParamsMapper(), + ).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + storedPaymentMethod = storedPaymentMethod, ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val publicKeyService = PublicKeyService(httpClient) val publicKeyRepository = DefaultPublicKeyRepository(publicKeyService) @@ -373,7 +484,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -386,11 +497,11 @@ constructor( analyticsRepository = analyticsRepository, cardEncryptor = cardEncryptor, publicKeyRepository = publicKeyRepository, - submitHandler = SubmitHandler(savedStateHandle) + submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -405,7 +516,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -427,6 +538,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + configuration: CardConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): CardComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt index 5d3a8b0c8d..3d63a5f088 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/CardDelegate.kt @@ -13,6 +13,9 @@ import com.adyen.checkout.card.BinLookupData import com.adyen.checkout.card.CardComponentState import com.adyen.checkout.card.internal.ui.model.CardInputData import com.adyen.checkout.card.internal.ui.model.CardOutputData +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.ui.core.internal.ui.AddressDelegate @@ -22,6 +25,7 @@ import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate import kotlinx.coroutines.flow.Flow @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@Suppress("TooManyFunctions") interface CardDelegate : PaymentComponentDelegate, ViewProvidingDelegate, @@ -44,4 +48,14 @@ interface CardDelegate : fun setOnBinValueListener(listener: ((binValue: String) -> Unit)?) fun setOnBinLookupListener(listener: ((data: List) -> Unit)?) + + fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) + + fun updateAddressLookupOptions(options: List) + + fun setAddressLookupResult(addressLookupResult: AddressLookupResult) + + fun handleBackPress(): Boolean + + fun startAddressLookup() } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/CardViewProvider.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/CardViewProvider.kt index db82d8cf7b..ab99c9dbe9 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/CardViewProvider.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/CardViewProvider.kt @@ -16,6 +16,7 @@ import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewProvider +import com.adyen.checkout.ui.core.internal.ui.view.AddressLookupView internal object CardViewProvider : ViewProvider { @@ -24,17 +25,24 @@ internal object CardViewProvider : ViewProvider { context: Context, ): ComponentView { return when (viewType) { - CardComponentViewType.DefaultCardView -> CardView(context) - CardComponentViewType.StoredCardView -> StoredCardView(context) + is CardComponentViewType.DefaultCardView -> CardView(context) + is CardComponentViewType.StoredCardView -> StoredCardView(context) + is CardComponentViewType.AddressLookup -> AddressLookupView(context) else -> throw IllegalArgumentException("Unsupported view type") } } } -internal sealed class CardComponentViewType : AmountButtonComponentViewType { - object DefaultCardView : CardComponentViewType() - object StoredCardView : CardComponentViewType() +internal sealed class CardComponentViewType : ComponentViewType { + data object DefaultCardView : CardComponentViewType(), AmountButtonComponentViewType { + override val buttonTextResId: Int = ButtonComponentViewType.DEFAULT_BUTTON_TEXT_RES_ID + } + + data object StoredCardView : CardComponentViewType(), AmountButtonComponentViewType { + override val buttonTextResId: Int = ButtonComponentViewType.DEFAULT_BUTTON_TEXT_RES_ID + } + + data object AddressLookup : CardComponentViewType() override val viewProvider: ViewProvider = CardViewProvider - override val buttonTextResId: Int = ButtonComponentViewType.DEFAULT_BUTTON_TEXT_RES_ID } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt index abe103653b..09525bf5be 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegate.kt @@ -35,6 +35,9 @@ import com.adyen.checkout.card.internal.util.DetectedCardTypesUtils import com.adyen.checkout.card.internal.util.InstallmentUtils import com.adyen.checkout.card.internal.util.KcpValidationUtils import com.adyen.checkout.card.internal.util.toBinLookupData +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentMethod @@ -43,14 +46,15 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.CardPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runCompileOnly import com.adyen.checkout.cse.EncryptedCard import com.adyen.checkout.cse.EncryptionException @@ -59,12 +63,12 @@ import com.adyen.checkout.cse.internal.BaseCardEncryptor import com.adyen.checkout.cse.internal.BaseGenericEncryptor import com.adyen.checkout.ui.core.internal.data.api.AddressRepository import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState +import com.adyen.checkout.ui.core.internal.ui.AddressLookupDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIState import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.ui.model.AddressParams @@ -100,7 +104,8 @@ class DefaultCardDelegate( private val cardEncryptor: BaseCardEncryptor, private val genericEncryptor: BaseGenericEncryptor, private val submitHandler: SubmitHandler, -) : CardDelegate { + private val addressLookupDelegate: AddressLookupDelegate +) : CardDelegate, AddressLookupDelegate by addressLookupDelegate { private val inputData: CardInputData = CardInputData() @@ -150,15 +155,24 @@ class DefaultCardDelegate( fetchPublicKey() subscribeToDetectedCardTypes() - if (componentParams.addressParams is AddressParams.FullAddress) { + if (componentParams.addressParams is AddressParams.FullAddress || + componentParams.addressParams is AddressParams.Lookup + ) { subscribeToStatesList() subscribeToCountryList() requestCountryList() } + addressLookupDelegate.addressLookupSubmitFlow + .onEach { + _viewFlow.tryEmit(CardComponentViewType.DefaultCardView) + inputData.address = it + updateOutputData() + } + .launchIn(coroutineScope) } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -175,7 +189,7 @@ class DefaultCardDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -184,21 +198,21 @@ class DefaultCardDelegate( } private fun fetchPublicKey() { - Logger.d(TAG, "fetchPublicKey") + adyenLog(AdyenLogLevel.DEBUG) { "fetchPublicKey" } coroutineScope.launch { publicKeyRepository.fetchPublicKey( environment = componentParams.environment, - clientKey = componentParams.clientKey + clientKey = componentParams.clientKey, ).fold( onSuccess = { key -> - Logger.d(TAG, "Public key fetched") + adyenLog(AdyenLogLevel.DEBUG) { "Public key fetched" } publicKey = key updateComponentState(outputData) }, onFailure = { e -> - Logger.e(TAG, "Unable to fetch public key") + adyenLog(AdyenLogLevel.ERROR) { "Unable to fetch public key" } exceptionChannel.trySend(ComponentException("Unable to fetch publicKey.", e)) - } + }, ) } } @@ -219,14 +233,14 @@ class DefaultCardDelegate( } private fun onInputDataChanged() { - Logger.v(TAG, "onInputDataChanged") + adyenLog(AdyenLogLevel.VERBOSE) { "onInputDataChanged" } detectCardTypeRepository.detectCardType( cardNumber = inputData.cardNumber, publicKey = publicKey, supportedCardBrands = componentParams.supportedCardBrands, clientKey = componentParams.clientKey, coroutineScope = coroutineScope, - type = paymentMethod.type + type = paymentMethod.type, ) requestStateList(inputData.address.country) } @@ -234,11 +248,10 @@ class DefaultCardDelegate( private fun subscribeToDetectedCardTypes() { detectCardTypeRepository.detectedCardTypesFlow .onEach { detectedCardTypes -> - Logger.d( - TAG, + adyenLog(AdyenLogLevel.DEBUG) { "New detected card types emitted - detectedCardTypes: ${detectedCardTypes.map { it.cardBrand }} " + "- isReliable: ${detectedCardTypes.firstOrNull()?.isReliable}" - ) + } if (detectedCardTypes != outputData.detectedCardTypes) { onBinLookupListener?.invoke(detectedCardTypes.map(DetectedCardType::toBinLookupData)) } @@ -254,11 +267,11 @@ class DefaultCardDelegate( addressRepository.countriesFlow .distinctUntilChanged() .onEach { countries -> - Logger.d(TAG, "New countries emitted - countries: ${countries.size}") + adyenLog(AdyenLogLevel.DEBUG) { "New countries emitted - countries: ${countries.size}" } val countryOptions = AddressFormUtils.initializeCountryOptions( shopperLocale = componentParams.shopperLocale, addressParams = componentParams.addressParams, - countryList = countries + countryList = countries, ) countryOptions.firstOrNull { it.selected }?.let { inputData.address.country = it.code @@ -273,8 +286,9 @@ class DefaultCardDelegate( addressRepository.statesFlow .distinctUntilChanged() .onEach { states -> - Logger.d(TAG, "New states emitted - states: ${states.size}") - updateOutputData(stateOptions = AddressFormUtils.initializeStateOptions(states)) + adyenLog(AdyenLogLevel.DEBUG) { "New states emitted - states: ${states.size}" } + val stateOptions = AddressFormUtils.initializeStateOptions(states) + updateOutputData(stateOptions = stateOptions) } .launchIn(coroutineScope) } @@ -284,7 +298,8 @@ class DefaultCardDelegate( countryOptions: List = outputData.addressState.countryOptions, stateOptions: List = outputData.addressState.stateOptions, ) { - val newOutputData = createOutputData(detectedCardTypes, countryOptions, stateOptions) + val newOutputData = + createOutputData(detectedCardTypes, countryOptions, stateOptions) _outputDataFlow.tryEmit(newOutputData) updateComponentState(newOutputData) } @@ -295,24 +310,24 @@ class DefaultCardDelegate( countryOptions: List = emptyList(), stateOptions: List = emptyList(), ): CardOutputData { - Logger.v(TAG, "createOutputData") + adyenLog(AdyenLogLevel.VERBOSE) { "createOutputData" } val updatedCountryOptions = AddressFormUtils.markAddressListItemSelected( countryOptions, - inputData.address.country + inputData.address.country, ) val updatedStateOptions = AddressFormUtils.markAddressListItemSelected( stateOptions, - inputData.address.stateOrProvince + inputData.address.stateOrProvince, ) val isReliable = detectedCardTypes.any { it.isReliable } val filteredDetectedCardTypes = DetectedCardTypesUtils.filterDetectedCardTypes( detectedCardTypes, - inputData.selectedCardIndex + inputData.selectedCardIndex, ) val selectedOrFirstCardType = DetectedCardTypesUtils.getSelectedOrFirstDetectedCardType( - detectedCardTypes = filteredDetectedCardTypes + detectedCardTypes = filteredDetectedCardTypes, ) // perform a Luhn Check if no brands are detected @@ -323,11 +338,19 @@ class DefaultCardDelegate( val addressFormUIState = AddressFormUIState.fromAddressParams(componentParams.addressParams) + val addressState = validateAddress( + inputData.address, + addressFormUIState, + selectedOrFirstCardType, + updatedCountryOptions, + updatedStateOptions, + ) + return CardOutputData( cardNumberState = validateCardNumber( cardNumber = inputData.cardNumber, enableLuhnCheck = enableLuhnCheck, - isBrandSupported = !shouldFailWithUnsupportedBrand + isBrandSupported = !shouldFailWithUnsupportedBrand, ), expiryDateState = validateExpiryDate(inputData.expiryDate, selectedOrFirstCardType?.expiryDatePolicy), securityCodeState = validateSecurityCode(inputData.securityCode, selectedOrFirstCardType), @@ -335,13 +358,7 @@ class DefaultCardDelegate( socialSecurityNumberState = validateSocialSecurityNumber(inputData.socialSecurityNumber), kcpBirthDateOrTaxNumberState = validateKcpBirthDateOrTaxNumber(inputData.kcpBirthDateOrTaxNumber), kcpCardPasswordState = validateKcpCardPassword(inputData.kcpCardPassword), - addressState = validateAddress( - inputData.address, - addressFormUIState, - selectedOrFirstCardType, - updatedCountryOptions, - updatedStateOptions - ), + addressState = addressState, installmentState = makeInstallmentFieldState(inputData.installmentOption), shouldStorePaymentMethod = inputData.isStorePaymentMethodSwitchChecked, cvcUIState = makeCvcUIState(selectedOrFirstCardType), @@ -355,12 +372,12 @@ class DefaultCardDelegate( installmentOptions = getInstallmentOptions( installmentParams = componentParams.installmentParams, cardBrand = selectedOrFirstCardType?.cardBrand, - isCardTypeReliable = isReliable + isCardTypeReliable = isReliable, ), cardBrands = getCardBrands(filteredDetectedCardTypes), isDualBranded = isDualBrandedFlow(filteredDetectedCardTypes), kcpBirthDateOrTaxNumberHint = getKcpBirthDateOrTaxNumberHint(inputData.kcpBirthDateOrTaxNumber), - isCardListVisible = isCardListVisible(getCardBrands(detectedCardTypes), filteredDetectedCardTypes) + isCardListVisible = isCardListVisible(getCardBrands(detectedCardTypes), filteredDetectedCardTypes), ) } @@ -377,7 +394,7 @@ class DefaultCardDelegate( @VisibleForTesting internal fun updateComponentState(outputData: CardOutputData) { - Logger.v(TAG, "updateComponentState") + adyenLog(AdyenLogLevel.VERBOSE) { "updateComponentState" } val componentState = createComponentState(outputData) _componentStateFlow.tryEmit(componentState) } @@ -389,7 +406,7 @@ class DefaultCardDelegate( val cardNumber = outputData.cardNumberState.value val firstCardBrand = DetectedCardTypesUtils.getSelectedOrFirstDetectedCardType( - detectedCardTypes = outputData.detectedCardTypes + detectedCardTypes = outputData.detectedCardTypes, )?.cardBrand val binValue = @@ -415,7 +432,7 @@ class DefaultCardDelegate( isReady = publicKey != null, cardBrand = firstCardBrand, binValue = binValue, - lastFourDigits = null + lastFourDigits = null, ) } @@ -431,7 +448,7 @@ class DefaultCardDelegate( if (expiryDateResult != ExpiryDate.EMPTY_DATE) { unencryptedCardBuilder.setExpiryDate( expiryMonth = expiryDateResult.expiryMonth.toString(), - expiryYear = expiryDateResult.expiryYear.toString() + expiryYear = expiryDateResult.expiryYear.toString(), ) } @@ -445,7 +462,7 @@ class DefaultCardDelegate( isReady = true, cardBrand = firstCardBrand, binValue = binValue, - lastFourDigits = null + lastFourDigits = null, ) } @@ -454,7 +471,7 @@ class DefaultCardDelegate( outputData, cardNumber, firstCardBrand, - binValue + binValue, ) } @@ -463,6 +480,20 @@ class DefaultCardDelegate( submitHandler.onSubmit(state = state) } + override fun startAddressLookup() { + _viewFlow.tryEmit(CardComponentViewType.AddressLookup) + addressLookupDelegate.initialize(coroutineScope, inputData.address) + } + + override fun handleBackPress(): Boolean { + return if (_viewFlow.value == CardComponentViewType.AddressLookup) { + _viewFlow.tryEmit(CardComponentViewType.DefaultCardView) + true + } else { + false + } + } + // Validation private fun validateCardNumber( cardNumber: String, @@ -492,12 +523,12 @@ class DefaultCardDelegate( return if (componentParams.isHolderNameRequired && holderName.isBlank()) { FieldState( holderName, - Validation.Invalid(R.string.checkout_holder_name_not_valid) + Validation.Invalid(R.string.checkout_holder_name_not_valid), ) } else { FieldState( holderName, - Validation.Valid + Validation.Valid, ) } } @@ -536,7 +567,7 @@ class DefaultCardDelegate( val isOptional = CardAddressValidationUtils.isAddressOptional( addressParams = componentParams.addressParams, - cardType = detectedCardType?.cardBrand?.txVariant + cardType = detectedCardType?.cardBrand?.txVariant, ) return AddressValidationUtils.validateAddressInput( @@ -544,7 +575,7 @@ class DefaultCardDelegate( addressFormUIState, countryOptions, stateOptions, - isOptional + isOptional, ) } @@ -592,7 +623,7 @@ class DefaultCardDelegate( private fun requestCountryList() { addressRepository.getCountryList( shopperLocale = componentParams.shopperLocale, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, ) } @@ -600,12 +631,12 @@ class DefaultCardDelegate( addressRepository.getStateList( shopperLocale = componentParams.shopperLocale, countryCode = countryCode, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, ) } private fun makeCvcUIState(detectedCardType: DetectedCardType?): InputFieldUIState { - Logger.d(TAG, "makeCvcUIState: ${detectedCardType?.cvcPolicy}") + adyenLog(AdyenLogLevel.DEBUG) { "makeCvcUIState: ${detectedCardType?.cvcPolicy}" } return if (detectedCardType?.isReliable == true) { when (componentParams.cvcVisibility) { @@ -676,7 +707,7 @@ class DefaultCardDelegate( encryptedPassword = genericEncryptor.encryptField( ENCRYPTION_KEY_FOR_KCP_PASSWORD, stateOutputData.kcpCardPasswordState.value, - publicKey + publicKey, ) } ?: throw CheckoutException("Encryption failed because public key cannot be found.") taxNumber = stateOutputData.kcpBirthDateOrTaxNumberState.value @@ -699,7 +730,7 @@ class DefaultCardDelegate( isReady = true, cardBrand = firstCardBrand, binValue = binValue, - lastFourDigits = lastFour + lastFourDigits = lastFour, ) } @@ -736,7 +767,7 @@ class DefaultCardDelegate( if (isAddressRequired(stateOutputData.addressUIState)) { billingAddress = AddressFormUtils.makeAddressData( addressOutputData = stateOutputData.addressState, - addressFormUIState = stateOutputData.addressUIState + addressFormUIState = stateOutputData.addressUIState, ) } if (isInstallmentsRequired(stateOutputData)) { @@ -763,7 +794,7 @@ class DefaultCardDelegate( private fun getCardBrand(detectedCardTypes: List): String? { return if (isDualBrandedFlow(detectedCardTypes)) { DetectedCardTypesUtils.getSelectedCardType( - detectedCardTypes = detectedCardTypes + detectedCardTypes = detectedCardTypes, ) } else { val reliableCardBrand = detectedCardTypes.firstOrNull { it.isReliable } @@ -784,15 +815,28 @@ class DefaultCardDelegate( onBinLookupListener = listener } + override fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) { + addressLookupDelegate.setAddressLookupCallback(addressLookupCallback) + } + + override fun updateAddressLookupOptions(options: List) { + adyenLog(AdyenLogLevel.DEBUG) { "update address lookup options $options" } + addressLookupDelegate.updateAddressLookupOptions(options) + } + + override fun setAddressLookupResult(addressLookupResult: AddressLookupResult) { + addressLookupDelegate.setAddressLookupResult(addressLookupResult) + } + override fun onCleared() { removeObserver() _coroutineScope = null onBinValueListener = null onBinLookupListener = null + addressLookupDelegate.clear() } companion object { - private val TAG = LogUtil.getTag() private const val DEBIT_FUNDING_SOURCE = "debit" @VisibleForTesting diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt index 200688e204..68b91590b1 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/StoredCardDelegate.kt @@ -23,6 +23,9 @@ import com.adyen.checkout.card.internal.ui.model.ExpiryDate import com.adyen.checkout.card.internal.ui.model.InputFieldUIState import com.adyen.checkout.card.internal.ui.model.StoredCVCVisibility import com.adyen.checkout.card.internal.util.CardValidationUtils +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentMethodTypes @@ -31,14 +34,15 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.CardPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runCompileOnly import com.adyen.checkout.cse.EncryptedCard import com.adyen.checkout.cse.EncryptionException @@ -50,7 +54,6 @@ import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIEvent import com.adyen.checkout.ui.core.internal.ui.PaymentComponentUIState import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.util.AddressValidationUtils import com.adyen.threeds2.ThreeDS2Service @@ -85,6 +88,7 @@ internal class StoredCardDelegate( cvcPolicy = when { componentParams.storedCVCVisibility == StoredCVCVisibility.HIDE || noCvcBrands.contains(cardType) -> Brand.FieldPolicy.HIDDEN + else -> Brand.FieldPolicy.REQUIRED }, expiryDatePolicy = Brand.FieldPolicy.REQUIRED, @@ -100,6 +104,7 @@ internal class StoredCardDelegate( override val addressOutputData: AddressOutputData get() = _outputDataFlow.value.addressState + override val addressOutputDataFlow: Flow get() = MutableStateFlow(_outputDataFlow.value.addressState) @@ -147,7 +152,7 @@ internal class StoredCardDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -164,7 +169,7 @@ internal class StoredCardDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -176,7 +181,7 @@ internal class StoredCardDelegate( coroutineScope?.launch { publicKeyRepository.fetchPublicKey( environment = componentParams.environment, - clientKey = componentParams.clientKey + clientKey = componentParams.clientKey, ).fold( onSuccess = { key -> publicKey = key @@ -184,7 +189,7 @@ internal class StoredCardDelegate( }, onFailure = { e -> exceptionChannel.trySend(ComponentException("Unable to fetch publicKey.", e)) - } + }, ) } } @@ -199,7 +204,7 @@ internal class StoredCardDelegate( } private fun onInputDataChanged() { - Logger.v(TAG, "onInputDataChanged") + adyenLog(AdyenLogLevel.VERBOSE) { "onInputDataChanged" } val outputData = createOutputData() _outputDataFlow.tryEmit(outputData) @@ -230,13 +235,13 @@ internal class StoredCardDelegate( cardBrands = emptyList(), isDualBranded = false, kcpBirthDateOrTaxNumberHint = null, - isCardListVisible = false + isCardListVisible = false, ) } @VisibleForTesting internal fun updateComponentState(outputData: CardOutputData) { - Logger.v(TAG, "updateComponentState") + adyenLog(AdyenLogLevel.VERBOSE) { "updateComponentState" } val componentState = createComponentState(outputData) _componentStateFlow.tryEmit(componentState) } @@ -259,7 +264,7 @@ internal class StoredCardDelegate( isReady = publicKey != null, cardBrand = firstCardBrand, binValue = "", - lastFourDigits = null + lastFourDigits = null, ) } @@ -274,7 +279,7 @@ internal class StoredCardDelegate( if (expiryDateResult != ExpiryDate.EMPTY_DATE) { unencryptedCardBuilder.setExpiryDate( expiryMonth = expiryDateResult.expiryMonth.toString(), - expiryYear = expiryDateResult.expiryYear.toString() + expiryYear = expiryDateResult.expiryYear.toString(), ) } @@ -287,7 +292,7 @@ internal class StoredCardDelegate( isReady = true, cardBrand = firstCardBrand, binValue = "", - lastFourDigits = null + lastFourDigits = null, ) } @@ -303,6 +308,12 @@ internal class StoredCardDelegate( submitHandler.onSubmit(state) } + override fun startAddressLookup() = Unit + + override fun handleBackPress(): Boolean { + return false + } + override fun getPaymentMethodType(): String { return storedPaymentMethod.type ?: PaymentMethodTypes.UNKNOWN } @@ -344,7 +355,7 @@ internal class StoredCardDelegate( isReady = true, cardBrand = firstCardBrand, binValue = "", - lastFourDigits = lastFour + lastFourDigits = lastFour, ) } @@ -365,11 +376,11 @@ internal class StoredCardDelegate( try { val storedDate = ExpiryDate( storedPaymentMethod.expiryMonth.orEmpty().toInt(), - storedPaymentMethod.expiryYear.orEmpty().toInt() + storedPaymentMethod.expiryYear.orEmpty().toInt(), ) inputData.expiryDate = storedDate } catch (e: NumberFormatException) { - Logger.e(TAG, "Failed to parse stored Date", e) + adyenLog(AdyenLogLevel.ERROR, e) { "Failed to parse stored Date" } inputData.expiryDate = ExpiryDate.EMPTY_DATE } @@ -377,7 +388,7 @@ internal class StoredCardDelegate( } private fun makeCvcUIState(cvcPolicy: Brand.FieldPolicy): InputFieldUIState { - Logger.d(TAG, "makeCvcUIState: $cvcPolicy") + adyenLog(AdyenLogLevel.DEBUG) { "makeCvcUIState: $cvcPolicy" } return when (cvcPolicy) { Brand.FieldPolicy.REQUIRED -> InputFieldUIState.REQUIRED Brand.FieldPolicy.OPTIONAL -> InputFieldUIState.OPTIONAL @@ -410,13 +421,18 @@ internal class StoredCardDelegate( // Bin lookup is not performed for stored cards override fun setOnBinLookupListener(listener: ((data: List) -> Unit)?) = Unit + override fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) = Unit + + override fun updateAddressLookupOptions(options: List) = Unit + + override fun setAddressLookupResult(addressLookupResult: AddressLookupResult) = Unit + override fun onCleared() { removeObserver() coroutineScope = null } companion object { - private val TAG = LogUtil.getTag() private const val LAST_FOUR_LENGTH = 4 } } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt index 36890caba5..2750ed6634 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParams.kt @@ -12,22 +12,14 @@ import androidx.annotation.RestrictTo import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.KCPAuthVisibility import com.adyen.checkout.card.SocialSecurityNumberVisibility -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment import com.adyen.checkout.ui.core.internal.ui.model.AddressParams -import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class CardComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, val isHolderNameRequired: Boolean, val supportedCardBrands: List, @@ -39,4 +31,4 @@ data class CardComponentParams( val addressParams: AddressParams, val cvcVisibility: CVCVisibility, val storedCVCVisibility: StoredCVCVisibility -) : ComponentParams, ButtonParams +) : ComponentParams by commonComponentParams, ButtonParams diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt index 726a6f57c6..44dda24c85 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapper.kt @@ -13,91 +13,114 @@ import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.CardConfiguration import com.adyen.checkout.card.KCPAuthVisibility import com.adyen.checkout.card.SocialSecurityNumberVisibility +import com.adyen.checkout.card.getCardConfiguration +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.model.AddressFieldPolicy import com.adyen.checkout.ui.core.internal.ui.model.AddressParams +import java.util.Locale @Suppress("TooManyFunctions") internal class CardComponentParamsMapper( + private val commonComponentParamsMapper: CommonComponentParamsMapper, private val installmentsParamsMapper: InstallmentsParamsMapper, - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, ) { - fun mapToParamsDefault( - cardConfiguration: CardConfiguration, + fun mapToParams( + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, paymentMethod: PaymentMethod, - sessionParams: SessionParams?, ): CardComponentParams { - val supportedCardBrands = cardConfiguration.getSupportedCardBrands(paymentMethod) - return mapToParams( - cardConfiguration, - supportedCardBrands, - sessionParams, + return mapToParamsInternal( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + paymentMethod, ) } - fun mapToParamsStored( - cardConfiguration: CardConfiguration, - sessionParams: SessionParams?, + fun mapToParams( + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + @Suppress("UNUSED_PARAMETER") storedPaymentMethod: StoredPaymentMethod, ): CardComponentParams { - val supportedCardBrands = cardConfiguration.getSupportedCardBrandsStored() + return mapToParamsInternal( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + null, + ) + } + + private fun mapToParamsInternal( + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + paymentMethod: PaymentMethod?, + ): CardComponentParams { + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + val cardConfiguration = checkoutConfiguration.getCardConfiguration() return mapToParams( + commonComponentParamsMapperData.commonComponentParams, + commonComponentParamsMapperData.sessionParams, cardConfiguration, - supportedCardBrands, - sessionParams, + paymentMethod, ) } private fun mapToParams( - cardConfiguration: CardConfiguration, - supportedCardBrands: List, + commonComponentParams: CommonComponentParams, sessionParams: SessionParams?, - ): CardComponentParams { - return cardConfiguration - .mapToParamsInternal(supportedCardBrands) - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } - - private fun CardConfiguration.mapToParamsInternal( - supportedCardBrands: List, + cardConfiguration: CardConfiguration?, + paymentMethod: PaymentMethod?, ): CardComponentParams { return CardComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - isHolderNameRequired = isHolderNameRequired ?: false, - isSubmitButtonVisible = isSubmitButtonVisible ?: true, - supportedCardBrands = supportedCardBrands, - shopperReference = shopperReference, - isStorePaymentFieldVisible = isStorePaymentFieldVisible ?: true, - socialSecurityNumberVisibility = socialSecurityNumberVisibility ?: SocialSecurityNumberVisibility.HIDE, - kcpAuthVisibility = kcpAuthVisibility ?: KCPAuthVisibility.HIDE, - installmentParams = installmentsParamsMapper.mapToInstallmentParams( - installmentConfiguration = installmentConfiguration, - amount = amount, - shopperLocale = shopperLocale + commonComponentParams = commonComponentParams, + isHolderNameRequired = cardConfiguration?.isHolderNameRequired ?: false, + isSubmitButtonVisible = cardConfiguration?.isSubmitButtonVisible ?: true, + supportedCardBrands = getSupportedCardBrands(cardConfiguration, paymentMethod), + shopperReference = cardConfiguration?.shopperReference, + isStorePaymentFieldVisible = getStorePaymentFieldVisible(sessionParams, cardConfiguration), + socialSecurityNumberVisibility = cardConfiguration?.socialSecurityNumberVisibility + ?: SocialSecurityNumberVisibility.HIDE, + kcpAuthVisibility = cardConfiguration?.kcpAuthVisibility ?: KCPAuthVisibility.HIDE, + installmentParams = getInstallmentParams( + sessionParams, + cardConfiguration, + commonComponentParams.amount, + commonComponentParams.shopperLocale, ), - addressParams = addressConfiguration?.mapToAddressParam() ?: AddressParams.None, - cvcVisibility = if (isHideCvc == true) { + addressParams = cardConfiguration?.addressConfiguration?.mapToAddressParam() ?: AddressParams.None, + cvcVisibility = if (cardConfiguration?.isHideCvc == true) { CVCVisibility.ALWAYS_HIDE } else { CVCVisibility.ALWAYS_SHOW }, - storedCVCVisibility = if (isHideCvcStoredCard == true) { + storedCVCVisibility = if (cardConfiguration?.isHideCvcStoredCard == true) { StoredCVCVisibility.HIDE } else { StoredCVCVisibility.SHOW - } + }, ) } @@ -106,49 +129,64 @@ internal class CardComponentParamsMapper( * Priority is: Custom -> PaymentMethod.brands -> Default * remove restricted card type */ - private fun CardConfiguration.getSupportedCardBrands( - paymentMethod: PaymentMethod + private fun getSupportedCardBrands( + cardConfiguration: CardConfiguration?, + paymentMethod: PaymentMethod? ): List { + val supportedCardBrands = cardConfiguration?.supportedCardBrands return when { !supportedCardBrands.isNullOrEmpty() -> { - Logger.v(TAG, "Reading supportedCardTypes from configuration") + adyenLog(AdyenLogLevel.VERBOSE) { "Reading supportedCardTypes from configuration" } supportedCardBrands } - paymentMethod.brands.orEmpty().isNotEmpty() -> { - Logger.v(TAG, "Reading supportedCardTypes from API brands") - paymentMethod.brands.orEmpty().map { + paymentMethod?.brands.orEmpty().isNotEmpty() -> { + adyenLog(AdyenLogLevel.VERBOSE) { "Reading supportedCardTypes from API brands" } + paymentMethod?.brands.orEmpty().map { CardBrand(txVariant = it) } } else -> { - Logger.v(TAG, "Falling back to CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST") + adyenLog(AdyenLogLevel.VERBOSE) { "Falling back to CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST" } CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST } }.removeRestrictedCards() } - private fun CardConfiguration.getSupportedCardBrandsStored(): List { - return supportedCardBrands.orEmpty().removeRestrictedCards() - } - private fun List.removeRestrictedCards(): List { return this.filter { !RestrictedCardType.isRestrictedCardType(it.txVariant) } } - private fun CardComponentParams.override( - overrideComponentParams: ComponentParams? - ): CardComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) + private fun getStorePaymentFieldVisible( + sessionParams: SessionParams?, + cardConfiguration: CardConfiguration?, + ): Boolean { + return sessionParams?.enableStoreDetails ?: cardConfiguration?.isStorePaymentFieldVisible ?: true + } + + private fun getInstallmentParams( + sessionParams: SessionParams?, + cardConfiguration: CardConfiguration?, + amount: Amount?, + shopperLocale: Locale + ): InstallmentParams? { + return if (sessionParams != null) { + // we don't fall back to the original value of installmentParams value on purpose + // if sessionParams.installmentOptions is null we want installmentParams to be also null regardless of what + // InstallmentConfiguration is passed to the mapper + installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = sessionParams.installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale, + ) + } else { + installmentsParamsMapper.mapToInstallmentParams( + installmentConfiguration = cardConfiguration?.installmentConfiguration, + amount = amount, + shopperLocale = shopperLocale, + ) + } } private fun AddressConfiguration.mapToAddressParam(): AddressParams { @@ -157,7 +195,7 @@ internal class CardComponentParamsMapper( AddressParams.FullAddress( defaultCountryCode, supportedCountryCodes, - addressFieldPolicy.mapToAddressParamFieldPolicy() + addressFieldPolicy.mapToAddressParamFieldPolicy(), ) } @@ -168,6 +206,10 @@ internal class CardComponentParamsMapper( is AddressConfiguration.PostalCode -> { AddressParams.PostalCode(addressFieldPolicy.mapToAddressParamFieldPolicy()) } + + is AddressConfiguration.Lookup -> { + AddressParams.Lookup() + } } } @@ -186,26 +228,4 @@ internal class CardComponentParamsMapper( } } } - - private fun CardComponentParams.override( - sessionParams: SessionParams? = null - ): CardComponentParams { - if (sessionParams == null) return this - return copy( - isStorePaymentFieldVisible = sessionParams.enableStoreDetails ?: isStorePaymentFieldVisible, - // we don't fall back to the original value of installmentParams value on purpose - // if sessionParams.installmentOptions is null we want installmentParams to be also null regardless of what - // InstallmentConfiguration is passed to the mapper - installmentParams = installmentsParamsMapper.mapToInstallmentParams( - installmentConfiguration = sessionParams.installmentConfiguration, - amount = sessionParams.amount ?: amount, - shopperLocale = shopperLocale - ), - amount = sessionParams.amount ?: amount, - ) - } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt index 52f0d917d0..a2220b0373 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardInputData.kt @@ -9,8 +9,9 @@ package com.adyen.checkout.card.internal.ui.model import androidx.annotation.RestrictTo import com.adyen.checkout.card.internal.ui.view.InstallmentModel +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.InputData -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupInputData @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class CardInputData( @@ -22,8 +23,9 @@ data class CardInputData( var kcpBirthDateOrTaxNumber: String = "", var kcpCardPassword: String = "", var postalCode: String = "", + var addressLookupInputData: AddressLookupInputData = AddressLookupInputData(), var address: AddressInputModel = AddressInputModel(), var isStorePaymentMethodSwitchChecked: Boolean = false, var selectedCardIndex: Int = -1, - var installmentOption: InstallmentModel? = null + var installmentOption: InstallmentModel? = null, ) : InputData diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt index 4a5858f99c..2808cf026b 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/model/CardOutputData.kt @@ -41,7 +41,7 @@ data class CardOutputData( val isDualBranded: Boolean, @StringRes val kcpBirthDateOrTaxNumberHint: Int?, - val isCardListVisible: Boolean + val isCardListVisible: Boolean, ) : OutputData { override val isValid: Boolean diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt index ba178b1cff..27c1ee85fc 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/CardView.kt @@ -11,6 +11,7 @@ import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.text.Editable +import android.text.InputType import android.util.AttributeSet import android.view.LayoutInflater import android.view.View @@ -41,6 +42,7 @@ import com.adyen.checkout.core.internal.util.BuildUtils import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.ui.loadLogo +import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.ui.view.AdyenTextInputEditText import com.adyen.checkout.ui.core.internal.ui.view.RoundCornerImageView import com.adyen.checkout.ui.core.internal.util.hideError @@ -65,7 +67,7 @@ class CardView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -108,6 +110,8 @@ class CardView @JvmOverloads constructor( observeDelegate(delegate, coroutineScope) + updateInputFields(cardDelegate.outputData) + initCardNumberInput() initExpiryDateInput() initSecurityCodeInput() @@ -116,6 +120,7 @@ class CardView @JvmOverloads constructor( initKcpAuthenticationInput() initPostalCodeInput() initAddressFormInput(coroutineScope) + initAddressLookup() binding.switchStorePaymentMethod.setOnCheckedChangeListener { _, isChecked -> delegate.updateInputData { isStorePaymentMethodSwitchChecked = isChecked } @@ -125,43 +130,47 @@ class CardView @JvmOverloads constructor( private fun initLocalizedStrings(localizedContext: Context) { binding.textInputLayoutCardNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_CardNumberInput, - localizedContext + localizedContext, ) binding.textInputLayoutExpiryDate.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_ExpiryDateInput, - localizedContext + localizedContext, ) binding.textInputLayoutSecurityCode.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_SecurityCodeInput, - localizedContext + localizedContext, ) binding.textInputLayoutCardHolder.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_HolderNameInput, - localizedContext + localizedContext, ) binding.textInputLayoutPostalCode.setLocalizedHintFromStyle( R.style.AdyenCheckout_PostalCodeInput, - localizedContext + localizedContext, + ) + binding.textInputLayoutAddressLookup.setLocalizedHintFromStyle( + R.style.AdyenCheckout_Card_AddressLookup_DropdownTextInputEditText, + localizedContext, ) binding.textInputLayoutSocialSecurityNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_SocialSecurityNumberInput, - localizedContext + localizedContext, ) binding.textInputLayoutKcpBirthDateOrTaxNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_KcpBirthDateOrTaxNumber, - localizedContext + localizedContext, ) binding.textInputLayoutKcpCardPassword.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_KcpCardPassword, - localizedContext + localizedContext, ) binding.textInputLayoutInstallments.setLocalizedHintFromStyle( R.style.AdyenCheckout_DropdownTextInputLayout_Installments, - localizedContext + localizedContext, ) binding.switchStorePaymentMethod.setLocalizedTextFromStyle( R.style.AdyenCheckout_Card_StorePaymentSwitch, - localizedContext + localizedContext, ) binding.addressFormInput.initLocalizedContext(localizedContext) } @@ -186,6 +195,7 @@ class CardView @JvmOverloads constructor( updateInstallments(cardOutputData) updateAddressHint(cardOutputData.addressUIState, cardOutputData.addressState.isOptional) setCardList(cardOutputData.cardBrands, cardOutputData.isCardListVisible) + updateAddressLookupInputText(cardOutputData.addressState) } @Suppress("ComplexMethod", "LongMethod") @@ -239,7 +249,7 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutSocialSecurityNumber.requestFocus() } binding.textInputLayoutSocialSecurityNumber.showError( - localizedContext.getString(socialSecurityNumberValidation.reason) + localizedContext.getString(socialSecurityNumberValidation.reason), ) } val kcpBirthDateOrTaxNumberValidation = it.kcpBirthDateOrTaxNumberState.validation @@ -251,7 +261,7 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutKcpBirthDateOrTaxNumber.requestFocus() } binding.textInputLayoutKcpBirthDateOrTaxNumber.showError( - localizedContext.getString(kcpBirthDateOrTaxNumberValidation.reason) + localizedContext.getString(kcpBirthDateOrTaxNumberValidation.reason), ) } val kcpPasswordValidation = it.kcpCardPasswordState.validation @@ -261,7 +271,7 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutKcpCardPassword.requestFocus() } binding.textInputLayoutKcpCardPassword.showError( - localizedContext.getString(kcpPasswordValidation.reason) + localizedContext.getString(kcpPasswordValidation.reason), ) } if (binding.addressFormInput.isVisible && !it.addressState.isValid) { @@ -483,7 +493,7 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutSocialSecurityNumber.hideError() } else if (socialSecurityNumberValidation is Validation.Invalid) { binding.textInputLayoutSocialSecurityNumber.showError( - localizedContext.getString(socialSecurityNumberValidation.reason) + localizedContext.getString(socialSecurityNumberValidation.reason), ) } } @@ -508,7 +518,7 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutKcpBirthDateOrTaxNumber.hideError() } else if (kcpBirthDateOrTaxNumberValidation is Validation.Invalid) { binding.textInputLayoutKcpBirthDateOrTaxNumber.showError( - localizedContext.getString(kcpBirthDateOrTaxNumberValidation.reason) + localizedContext.getString(kcpBirthDateOrTaxNumberValidation.reason), ) } } @@ -527,7 +537,7 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutKcpCardPassword.hideError() } else if (kcpBirthDateOrRegistrationNumberValidation is Validation.Invalid) { binding.textInputLayoutKcpCardPassword.showError( - localizedContext.getString(kcpBirthDateOrRegistrationNumberValidation.reason) + localizedContext.getString(kcpBirthDateOrRegistrationNumberValidation.reason), ) } } @@ -554,6 +564,16 @@ class CardView @JvmOverloads constructor( binding.addressFormInput.attachDelegate(cardDelegate, coroutineScope) } + private fun initAddressLookup() { + binding.autoCompleteTextViewAddressLookup.apply { + inputType = InputType.TYPE_NULL + isSingleLine = false + } + binding.autoCompleteTextViewAddressLookup.setOnClickListener { + cardDelegate.startAddressLookup() + } + } + private fun updateInstallments(cardOutputData: CardOutputData) { val installmentTextInputLayout = binding.textInputLayoutInstallments val installmentAutoCompleteTextView = binding.autoCompleteTextViewInstallments @@ -565,7 +585,7 @@ class CardView @JvmOverloads constructor( updateInstallmentSelection(cardOutputData.installmentOptions.first()) val installmentOptionText = InstallmentUtils.getTextForInstallmentOption( localizedContext, - cardOutputData.installmentOptions.first() + cardOutputData.installmentOptions.first(), ) installmentAutoCompleteTextView.setText(installmentOptionText) } @@ -595,14 +615,14 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutSecurityCode.isVisible = true binding.textInputLayoutSecurityCode.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_SecurityCodeInput, - localizedContext + localizedContext, ) } InputFieldUIState.OPTIONAL -> { binding.textInputLayoutSecurityCode.isVisible = true binding.textInputLayoutSecurityCode.hint = localizedContext.getString( - R.string.checkout_card_security_code_optional_hint + R.string.checkout_card_security_code_optional_hint, ) } @@ -623,14 +643,14 @@ class CardView @JvmOverloads constructor( binding.textInputLayoutExpiryDate.isVisible = true binding.textInputLayoutExpiryDate.setLocalizedHintFromStyle( R.style.AdyenCheckout_Card_ExpiryDateInput, - localizedContext + localizedContext, ) } InputFieldUIState.OPTIONAL -> { binding.textInputLayoutExpiryDate.isVisible = true binding.textInputLayoutExpiryDate.hint = localizedContext.getString( - R.string.checkout_card_expiry_date_optional_hint + R.string.checkout_card_expiry_date_optional_hint, ) } @@ -668,22 +688,35 @@ class CardView @JvmOverloads constructor( private fun setAddressInputVisibility(addressFormUIState: AddressFormUIState) { when (addressFormUIState) { AddressFormUIState.FULL_ADDRESS -> { - binding.addressFormInput.isVisible = true binding.textInputLayoutPostalCode.isVisible = false + binding.textInputLayoutAddressLookup.isVisible = false + binding.addressFormInput.isVisible = true } AddressFormUIState.POSTAL_CODE -> { binding.addressFormInput.isVisible = false + binding.textInputLayoutAddressLookup.isVisible = false binding.textInputLayoutPostalCode.isVisible = true } AddressFormUIState.NONE -> { binding.addressFormInput.isVisible = false binding.textInputLayoutPostalCode.isVisible = false + binding.textInputLayoutAddressLookup.isVisible = false + } + + AddressFormUIState.LOOKUP -> { + binding.addressFormInput.isVisible = false + binding.textInputLayoutPostalCode.isVisible = false + binding.textInputLayoutAddressLookup.isVisible = true } } } + private fun updateAddressLookupInputText(addressOutputData: AddressOutputData) { + binding.autoCompleteTextViewAddressLookup.setText(addressOutputData.toString()) + } + private fun updateAddressHint(addressFormUIState: AddressFormUIState, isOptional: Boolean) { when (addressFormUIState) { AddressFormUIState.FULL_ADDRESS -> binding.addressFormInput.updateAddressHint(isOptional) @@ -708,6 +741,21 @@ class CardView @JvmOverloads constructor( } } + private fun updateInputFields(cardOutputData: CardOutputData?) { + cardOutputData?.let { outputData -> + binding.editTextCardNumber.setText(outputData.cardNumberState.value) + binding.editTextExpiryDate.date = outputData.expiryDateState.value + binding.editTextSecurityCode.setText(outputData.securityCodeState.value) + binding.editTextCardHolder.setText(outputData.holderNameState.value) + binding.editTextSocialSecurityNumber.setSocialSecurityNumber(outputData.socialSecurityNumberState.value) + binding.editTextKcpBirthDateOrTaxNumber.setText(outputData.kcpBirthDateOrTaxNumberState.value) + binding.editTextKcpCardPassword.setText(outputData.kcpCardPasswordState.value) + binding.autoCompleteTextViewInstallments.setText( + InstallmentUtils.getTextForInstallmentOption(localizedContext, outputData.installmentState.value), + ) + } + } + private fun getActivity(context: Context): Activity? { return when (context) { is Activity -> context diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/ExpiryDateInput.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/ExpiryDateInput.kt index 486a6d14ca..bffb91417c 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/ExpiryDateInput.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/ExpiryDateInput.kt @@ -12,9 +12,9 @@ import android.os.Build import android.text.Editable import android.util.AttributeSet import com.adyen.checkout.card.internal.ui.model.ExpiryDate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.internal.util.StringUtil.normalize +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.view.AdyenTextInputEditText import java.text.ParseException import java.text.SimpleDateFormat @@ -60,7 +60,7 @@ class ExpiryDateInput @JvmOverloads constructor( var date: ExpiryDate get() { val normalizedExpiryDate = normalize(rawValue) - Logger.v(TAG, "getDate - $normalizedExpiryDate") + adyenLog(AdyenLogLevel.VERBOSE) { "getDate - $normalizedExpiryDate" } return try { val parsedDate = requireNotNull(dateFormat.parse(normalizedExpiryDate)) val calendar = GregorianCalendar.getInstance() @@ -69,13 +69,13 @@ class ExpiryDateInput @JvmOverloads constructor( // GregorianCalendar is 0 based ExpiryDate(calendar[Calendar.MONTH] + 1, calendar[Calendar.YEAR]) } catch (e: ParseException) { - Logger.d(TAG, "getDate - value does not match expected pattern. " + e.localizedMessage) + adyenLog(AdyenLogLevel.DEBUG, e) { "getDate - value does not match expected pattern. " } if (rawValue.isEmpty()) ExpiryDate.EMPTY_DATE else ExpiryDate.INVALID_DATE } } set(expiryDate) { if (expiryDate !== ExpiryDate.EMPTY_DATE) { - Logger.v(TAG, "setDate - " + expiryDate.expiryYear + " " + expiryDate.expiryMonth) + adyenLog(AdyenLogLevel.VERBOSE) { "setDate - " + expiryDate.expiryYear + " " + expiryDate.expiryMonth } val calendar = GregorianCalendar.getInstance() calendar.clear() // first day of month, GregorianCalendar month is 0 based. @@ -110,7 +110,6 @@ class ExpiryDateInput @JvmOverloads constructor( } companion object { - private val TAG = LogUtil.getTag() const val SEPARATOR = "/" private const val DATE_FORMAT = "MM" + SEPARATOR + "yy" private const val MAX_LENGTH = 5 diff --git a/card/src/main/java/com/adyen/checkout/card/internal/util/CardAddressValidationUtils.kt b/card/src/main/java/com/adyen/checkout/card/internal/util/CardAddressValidationUtils.kt index 2d03c293db..2577f750bf 100644 --- a/card/src/main/java/com/adyen/checkout/card/internal/util/CardAddressValidationUtils.kt +++ b/card/src/main/java/com/adyen/checkout/card/internal/util/CardAddressValidationUtils.kt @@ -27,6 +27,9 @@ internal object CardAddressValidationUtils { AddressParams.None -> { true } + is AddressParams.Lookup -> { + false + } } ?: true } diff --git a/card/src/main/res/layout/card_view.xml b/card/src/main/res/layout/card_view.xml index 8ba391e68c..80ff6dad6e 100644 --- a/card/src/main/res/layout/card_view.xml +++ b/card/src/main/res/layout/card_view.xml @@ -150,6 +150,23 @@ android:nextFocusForward="@id/editText_socialSecurityNumber" /> + + + + + + 2 - + + + diff --git a/card/src/test/java/com/adyen/checkout/card/CardConfigurationTest.kt b/card/src/test/java/com/adyen/checkout/card/CardConfigurationTest.kt new file mode 100644 index 0000000000..a4a202785f --- /dev/null +++ b/card/src/test/java/com/adyen/checkout/card/CardConfigurationTest.kt @@ -0,0 +1,138 @@ +package com.adyen.checkout.card + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class CardConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + card { + setSupportedCardTypes(CardType.MASTERCARD) + setHolderNameRequired(true) + setShowStorePaymentField(true) + setShopperReference("shopperReference") + setHideCvc(true) + setHideCvcStoredCard(true) + setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + setKcpAuthVisibility(KCPAuthVisibility.SHOW) + setInstallmentConfigurations(InstallmentConfiguration(showInstallmentAmount = true)) + setAddressConfiguration(AddressConfiguration.None) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getCardConfiguration() + + val expected = CardConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSupportedCardTypes(CardType.MASTERCARD) + .setHolderNameRequired(true) + .setShowStorePaymentField(true) + .setShopperReference("shopperReference") + .setHideCvc(true) + .setHideCvcStoredCard(true) + .setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + .setKcpAuthVisibility(KCPAuthVisibility.SHOW) + .setInstallmentConfigurations(InstallmentConfiguration(showInstallmentAmount = true)) + .setAddressConfiguration(AddressConfiguration.None) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.supportedCardBrands, actual?.supportedCardBrands) + assertEquals(expected.isHolderNameRequired, actual?.isHolderNameRequired) + assertEquals(expected.isStorePaymentFieldVisible, actual?.isStorePaymentFieldVisible) + assertEquals(expected.shopperReference, actual?.shopperReference) + assertEquals(expected.isHideCvc, actual?.isHideCvc) + assertEquals(expected.isHideCvcStoredCard, actual?.isHideCvcStoredCard) + assertEquals(expected.socialSecurityNumberVisibility, actual?.socialSecurityNumberVisibility) + assertEquals(expected.kcpAuthVisibility, actual?.kcpAuthVisibility) + assertEquals(expected.installmentConfiguration, actual?.installmentConfiguration) + assertEquals(expected.addressConfiguration, actual?.addressConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = CardConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSupportedCardTypes(CardType.MASTERCARD) + .setHolderNameRequired(true) + .setShowStorePaymentField(true) + .setShopperReference("shopperReference") + .setHideCvc(true) + .setHideCvcStoredCard(true) + .setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + .setKcpAuthVisibility(KCPAuthVisibility.SHOW) + .setInstallmentConfigurations(InstallmentConfiguration(showInstallmentAmount = true)) + .setAddressConfiguration(AddressConfiguration.None) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualCardConfig = actual.getCardConfiguration() + assertEquals(config.shopperLocale, actualCardConfig?.shopperLocale) + assertEquals(config.environment, actualCardConfig?.environment) + assertEquals(config.clientKey, actualCardConfig?.clientKey) + assertEquals(config.amount, actualCardConfig?.amount) + assertEquals(config.analyticsConfiguration, actualCardConfig?.analyticsConfiguration) + assertEquals(config.supportedCardBrands, actualCardConfig?.supportedCardBrands) + assertEquals(config.isHolderNameRequired, actualCardConfig?.isHolderNameRequired) + assertEquals(config.isStorePaymentFieldVisible, actualCardConfig?.isStorePaymentFieldVisible) + assertEquals(config.shopperReference, actualCardConfig?.shopperReference) + assertEquals(config.isHideCvc, actualCardConfig?.isHideCvc) + assertEquals(config.isHideCvcStoredCard, actualCardConfig?.isHideCvcStoredCard) + assertEquals(config.socialSecurityNumberVisibility, actualCardConfig?.socialSecurityNumberVisibility) + assertEquals(config.kcpAuthVisibility, actualCardConfig?.kcpAuthVisibility) + assertEquals(config.installmentConfiguration, actualCardConfig?.installmentConfiguration) + assertEquals(config.addressConfiguration, actualCardConfig?.addressConfiguration) + assertEquals(config.isSubmitButtonVisible, actualCardConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt index 94fb94fc04..d5cb26abb3 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/ui/DefaultCardDelegateTest.kt @@ -21,6 +21,7 @@ import com.adyen.checkout.card.InstallmentOptions import com.adyen.checkout.card.KCPAuthVisibility import com.adyen.checkout.card.R import com.adyen.checkout.card.SocialSecurityNumberVisibility +import com.adyen.checkout.card.card import com.adyen.checkout.card.internal.data.api.DetectCardTypeRepository import com.adyen.checkout.card.internal.data.api.TestDetectCardTypeRepository import com.adyen.checkout.card.internal.data.api.TestDetectedCardType @@ -40,6 +41,7 @@ import com.adyen.checkout.card.internal.ui.view.InstallmentModel import com.adyen.checkout.card.internal.util.DetectedCardTypesUtils import com.adyen.checkout.card.internal.util.InstallmentUtils import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.PaymentMethodTypes @@ -47,6 +49,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.core.Environment @@ -58,14 +62,15 @@ import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.ui.core.internal.data.api.AddressRepository import com.adyen.checkout.ui.core.internal.test.TestAddressRepository import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState +import com.adyen.checkout.ui.core.internal.ui.AddressLookupDelegate import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.ui.model.AddressParams import com.adyen.checkout.ui.core.internal.util.AddressFormUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest @@ -92,7 +97,8 @@ import java.util.Locale @ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) internal class DefaultCardDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, - @Mock private val submitHandler: SubmitHandler + @Mock private val submitHandler: SubmitHandler, + @Mock private val addressLookupDelegate: AddressLookupDelegate, ) { private lateinit var cardEncryptor: TestCardEncryptor @@ -109,6 +115,9 @@ internal class DefaultCardDelegateTest( publicKeyRepository = TestPublicKeyRepository() addressRepository = TestAddressRepository() detectCardTypeRepository = TestDetectCardTypeRepository() + + whenever(addressLookupDelegate.addressLookupSubmitFlow).thenReturn(MutableStateFlow(AddressInputModel())) + delegate = createCardDelegate() } @@ -149,9 +158,9 @@ internal class DefaultCardDelegateTest( val countriesFlow = addressRepository.countriesFlow.testIn(this) val statesFlow = addressRepository.statesFlow.testIn(this) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setAddressConfiguration(AddressConfiguration.PostalCode()) - .build() + configuration = createCheckoutConfiguration { + setAddressConfiguration(AddressConfiguration.PostalCode()) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -170,9 +179,9 @@ internal class DefaultCardDelegateTest( addressRepository.shouldReturnError = true delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setAddressConfiguration(AddressConfiguration.FullAddress()) - .build() + configuration = createCheckoutConfiguration { + setAddressConfiguration(AddressConfiguration.FullAddress()) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -190,9 +199,9 @@ internal class DefaultCardDelegateTest( val statesFlow = addressRepository.statesFlow.testIn(this) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setAddressConfiguration(AddressConfiguration.FullAddress(defaultCountryCode = "NL")) - .build() + configuration = createCheckoutConfiguration { + setAddressConfiguration(AddressConfiguration.FullAddress(defaultCountryCode = "NL")) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -210,9 +219,9 @@ internal class DefaultCardDelegateTest( val statesFlow = addressRepository.statesFlow.testIn(this) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder(shopperLocale = Locale.CANADA) - .setAddressConfiguration(AddressConfiguration.FullAddress()) - .build() + configuration = createCheckoutConfiguration(shopperLocale = Locale.CANADA) { + setAddressConfiguration(AddressConfiguration.FullAddress()) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -230,7 +239,7 @@ internal class DefaultCardDelegateTest( val countryOptions = AddressFormUtils.initializeCountryOptions( shopperLocale = delegate.componentParams.shopperLocale, addressParams = addressParams, - countryList = TestAddressRepository.COUNTRIES + countryList = TestAddressRepository.COUNTRIES, ) val expectedCountries = AddressFormUtils.markAddressListItemSelected( @@ -239,9 +248,9 @@ internal class DefaultCardDelegateTest( ) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setAddressConfiguration(addressConfiguration) - .build() + configuration = createCheckoutConfiguration { + setAddressConfiguration(addressConfiguration) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -253,7 +262,7 @@ internal class DefaultCardDelegateTest( houseNumberOrName = "44", apartmentSuite = "aparment", city = "Istanbul", - country = "Turkey" + country = "Turkey", ) delegate.addressOutputDataFlow.test { @@ -271,7 +280,7 @@ internal class DefaultCardDelegateTest( assertEquals(expectedCountries, countryOptions) assertEquals( stateOptions, - AddressFormUtils.initializeStateOptions(TestAddressRepository.STATES) + AddressFormUtils.initializeStateOptions(TestAddressRepository.STATES), ) } } @@ -295,9 +304,9 @@ internal class DefaultCardDelegateTest( fun `When a card brand is detected, isCardListVisible should be false`() = runTest { val supportedCardBrands = listOf(CardBrand(cardType = CardType.VISA)) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSupportedCardTypes(*supportedCardBrands.toTypedArray()) - .build() + configuration = createCheckoutConfiguration { + setSupportedCardTypes(*supportedCardBrands.toTypedArray()) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -314,9 +323,9 @@ internal class DefaultCardDelegateTest( fun `When a card brand is not detected, isCardListVisible should be true`() = runTest { val supportedCardBrands = listOf(CardBrand(cardType = CardType.VISA)) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSupportedCardTypes(*supportedCardBrands.toTypedArray()) - .build() + configuration = createCheckoutConfiguration { + setSupportedCardTypes(*supportedCardBrands.toTypedArray()) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) detectCardTypeRepository.detectionResult = TestDetectedCardType.EMPTY @@ -334,9 +343,9 @@ internal class DefaultCardDelegateTest( fun `When the supported card list is empty, isCardListVisible should be true`() = runTest { val supportedCardBrands = emptyList() delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSupportedCardTypes(*supportedCardBrands.toTypedArray()) - .build() + configuration = createCheckoutConfiguration { + setSupportedCardTypes(*supportedCardBrands.toTypedArray()) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) detectCardTypeRepository.detectionResult = TestDetectedCardType.EMPTY @@ -355,12 +364,12 @@ internal class DefaultCardDelegateTest( val supportedCardBrands = listOf( CardBrand(cardType = CardType.VISA), CardBrand(cardType = CardType.MASTERCARD), - CardBrand(cardType = CardType.AMERICAN_EXPRESS) + CardBrand(cardType = CardType.AMERICAN_EXPRESS), ) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSupportedCardTypes(*supportedCardBrands.toTypedArray()) - .build() + configuration = createCheckoutConfiguration { + setSupportedCardTypes(*supportedCardBrands.toTypedArray()) + }, ) detectCardTypeRepository.detectionResult = TestDetectedCardType.DETECTED_LOCALLY @@ -382,12 +391,12 @@ internal class DefaultCardDelegateTest( fun `detect card type repository returns unsupported cards, then output data should filter them`() = runTest { val supportedCardTypes = listOf( CardBrand(cardType = CardType.VISA), - CardBrand(cardType = CardType.AMERICAN_EXPRESS) + CardBrand(cardType = CardType.AMERICAN_EXPRESS), ) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSupportedCardTypes(*supportedCardTypes.toTypedArray()) - .build() + configuration = createCheckoutConfiguration { + setSupportedCardTypes(*supportedCardTypes.toTypedArray()) + }, ) detectCardTypeRepository.detectionResult = TestDetectedCardType.FETCHED_FROM_NETWORK @@ -410,12 +419,12 @@ internal class DefaultCardDelegateTest( fun `detect card type repository returns dual branded cards, then output data should be good`() = runTest { val supportedCardBrands = listOf( CardBrand(cardType = CardType.BCMC), - CardBrand(cardType = CardType.MAESTRO) + CardBrand(cardType = CardType.MAESTRO), ) delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSupportedCardTypes(*supportedCardBrands.toTypedArray()) - .build() + configuration = createCheckoutConfiguration { + setSupportedCardTypes(*supportedCardBrands.toTypedArray()) + }, ) detectCardTypeRepository.detectionResult = TestDetectedCardType.DUAL_BRANDED @@ -482,7 +491,7 @@ internal class DefaultCardDelegateTest( @Test fun `input is empty with custom config, then output data should be invalid`() = runTest { delegate = createCardDelegate( - configuration = getCustomCardConfigurationBuilder().build() + configuration = getCustomCardConfiguration(), ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -528,38 +537,38 @@ internal class DefaultCardDelegateTest( val cardBrands = listOf( CardListItem(CardBrand(cardType = CardType.VISA), true, Environment.TEST), CardListItem(CardBrand(cardType = CardType.MASTERCARD), false, Environment.TEST), - CardListItem(CardBrand(cardType = CardType.AMERICAN_EXPRESS), false, Environment.TEST) + CardListItem(CardBrand(cardType = CardType.AMERICAN_EXPRESS), false, Environment.TEST), ) val supportedCardBrands = cardBrands.map { it.cardBrand } val installmentConfiguration = InstallmentConfiguration( InstallmentOptions.DefaultInstallmentOptions( maxInstallments = 3, - includeRevolving = true - ) + includeRevolving = true, + ), ) val expectedInstallmentParams = InstallmentParams( InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2, 3), - includeRevolving = true + includeRevolving = true, ), - shopperLocale = Locale.US + shopperLocale = Locale.US, ) val addressConfiguration = AddressConfiguration.FullAddress() val addressParams = AddressParams.FullAddress(addressFieldPolicy = AddressFieldPolicyParams.Required) delegate = createCardDelegate( - configuration = CardConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY) - .setHideCvc(true) - .setHideCvcStoredCard(true) - .setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) - .setInstallmentConfigurations(installmentConfiguration) - .setHolderNameRequired(true) - .setAddressConfiguration(addressConfiguration) - .setKcpAuthVisibility(KCPAuthVisibility.SHOW) - .setSupportedCardTypes(*supportedCardBrands.toTypedArray()) - .setShowStorePaymentField(false) - .build() + configuration = createCheckoutConfiguration { + setHideCvc(true) + setHideCvcStoredCard(true) + setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + setInstallmentConfigurations(installmentConfiguration) + setHolderNameRequired(true) + setAddressConfiguration(addressConfiguration) + setKcpAuthVisibility(KCPAuthVisibility.SHOW) + setSupportedCardTypes(*supportedCardBrands.toTypedArray()) + setShowStorePaymentField(false) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -570,7 +579,7 @@ internal class DefaultCardDelegateTest( option = InstallmentOption.REVOLVING, amount = null, shopperLocale = Locale.US, - showAmount = false + showAmount = false, ) delegate.updateInputData { @@ -598,7 +607,7 @@ internal class DefaultCardDelegateTest( val countryOptions = AddressFormUtils.initializeCountryOptions( shopperLocale = delegate.componentParams.shopperLocale, addressParams = addressParams, - countryList = TestAddressRepository.COUNTRIES + countryList = TestAddressRepository.COUNTRIES, ) val expectedCountries = AddressFormUtils.markAddressListItemSelected( @@ -616,7 +625,7 @@ internal class DefaultCardDelegateTest( country = FieldState("Netherlands", Validation.Valid), isOptional = false, countryOptions = expectedCountries, - stateOptions = AddressFormUtils.initializeStateOptions(TestAddressRepository.STATES) + stateOptions = AddressFormUtils.initializeStateOptions(TestAddressRepository.STATES), ) val expectedDetectedCardTypes = detectCardTypeRepository.getDetectedCardTypesLocal(supportedCardBrands) @@ -624,7 +633,7 @@ internal class DefaultCardDelegateTest( val expectedInstallmentOptions = InstallmentUtils.makeInstallmentOptions( expectedInstallmentParams, expectedDetectedCardTypes.first().cardBrand, - true + true, ) val expectedOutputData = createOutputData( @@ -649,7 +658,7 @@ internal class DefaultCardDelegateTest( installmentOptions = expectedInstallmentOptions, kcpBirthDateOrTaxNumberHint = R.string.checkout_kcp_tax_number_hint, cardBrands = cardBrands, - isCardListVisible = false + isCardListVisible = false, ) with(expectMostRecentItem()) { @@ -702,9 +711,9 @@ internal class DefaultCardDelegateTest( createOutputData( cardNumberState = FieldState( "12345678", - Validation.Invalid(R.string.checkout_card_number_not_valid) - ) - ) + Validation.Invalid(R.string.checkout_card_number_not_valid), + ), + ), ) val componentState = expectMostRecentItem() @@ -724,9 +733,9 @@ internal class DefaultCardDelegateTest( createOutputData( expiryDateState = FieldState( ExpiryDate(10, 2020), - Validation.Invalid(R.string.checkout_expiry_date_not_valid) - ) - ) + Validation.Invalid(R.string.checkout_expiry_date_not_valid), + ), + ), ) val componentState = expectMostRecentItem() @@ -789,7 +798,7 @@ internal class DefaultCardDelegateTest( fun `output data with custom config is valid, then component state should be good`() = runTest { delegate = createCardDelegate( paymentMethod = PaymentMethod(fundingSource = "funding_source_1"), - configuration = getCustomCardConfigurationBuilder().build(), + configuration = getCustomCardConfiguration(), ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -803,7 +812,7 @@ internal class DefaultCardDelegateTest( apartmentSuite = FieldState("apt", Validation.Valid), city = FieldState("Amsterdam", Validation.Valid), country = FieldState("Netherlands", Validation.Valid), - isOptional = false + isOptional = false, ) val addressUIState = AddressFormUIState.FULL_ADDRESS @@ -812,15 +821,15 @@ internal class DefaultCardDelegateTest( option = InstallmentOption.REVOLVING, amount = null, shopperLocale = Locale.US, - showAmount = false + showAmount = false, ) val detectedCardTypes = listOf( createDetectedCardType(), createDetectedCardType( isSelected = true, - cardBrand = CardBrand(cardType = CardType.VISA) - ) + cardBrand = CardBrand(cardType = CardType.VISA), + ), ) delegate.updateComponentState( @@ -842,9 +851,9 @@ internal class DefaultCardDelegateTest( cardBrands = listOf( CardListItem(CardBrand(cardType = CardType.VISA), false, Environment.TEST), CardListItem(CardBrand(cardType = CardType.MASTERCARD), false, Environment.TEST), - CardListItem(CardBrand(cardType = CardType.AMERICAN_EXPRESS), false, Environment.TEST) + CardListItem(CardBrand(cardType = CardType.AMERICAN_EXPRESS), false, Environment.TEST), ), - ) + ), ) val componentState = expectMostRecentItem() @@ -895,7 +904,7 @@ internal class DefaultCardDelegateTest( delegate.componentStateFlow.test { delegate.updateComponentState( - createOutputData(cardNumberState = FieldState("12345678901234", Validation.Valid)) + createOutputData(cardNumberState = FieldState("12345678901234", Validation.Valid)), ) val componentState = expectMostRecentItem() @@ -910,7 +919,7 @@ internal class DefaultCardDelegateTest( delegate.componentStateFlow.test { delegate.updateComponentState( - createOutputData(cardNumberState = FieldState("1234567890123456", Validation.Valid)) + createOutputData(cardNumberState = FieldState("1234567890123456", Validation.Valid)), ) val componentState = expectMostRecentItem() @@ -926,9 +935,9 @@ internal class DefaultCardDelegateTest( isStorePaymentMethodSwitchChecked: Boolean, expectedStorePaymentMethod: Boolean?, ) = runTest { - val configuration = getDefaultCardConfigurationBuilder() - .setShowStorePaymentField(isStorePaymentMethodSwitchVisible) - .build() + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(isStorePaymentMethodSwitchVisible) + } delegate = createCardDelegate(configuration = configuration) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -952,9 +961,7 @@ internal class DefaultCardDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultCardConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(amount = configurationValue) delegate = createCardDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -981,9 +988,9 @@ internal class DefaultCardDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -992,9 +999,9 @@ internal class DefaultCardDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -1156,6 +1163,41 @@ internal class DefaultCardDelegateTest( } } + @Test + fun `when startAddressLookup is called view flow should emit AddressLookup`() = runTest { + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + delegate.startAddressLookup() + delegate.viewFlow.test { + assertEquals(CardComponentViewType.AddressLookup, awaitItem()) + expectNoEvents() + } + } + + @Test + fun `when view type is AddressLookup and handleBackPress() is called DefaultCardView should be emitted`() = + runTest { + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + delegate.startAddressLookup() + assertTrue(delegate.handleBackPress()) + delegate.viewFlow.test { + assertEquals(CardComponentViewType.DefaultCardView, awaitItem()) + expectNoEvents() + } + } + + @Test + fun `when view type is DefaultCardView and handleBackPress() is called it should return false`() = runTest { + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + assertFalse(delegate.handleBackPress()) + } + + @Test + fun `when delegate is cleared then address lookup delegate is cleared`() = runTest { + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + delegate.onCleared() + verify(addressLookupDelegate).clear() + } + @Suppress("LongParameterList") private fun createCardDelegate( publicKeyRepository: PublicKeyRepository = this.publicKeyRepository, @@ -1164,17 +1206,23 @@ internal class DefaultCardDelegateTest( cardValidationMapper: CardValidationMapper = CardValidationMapper(), cardEncryptor: BaseCardEncryptor = this.cardEncryptor, genericEncryptor: BaseGenericEncryptor = this.genericEncryptor, - configuration: CardConfiguration = getDefaultCardConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), paymentMethod: PaymentMethod = PaymentMethod(type = PaymentMethodTypes.SCHEME), analyticsRepository: AnalyticsRepository = this.analyticsRepository, submitHandler: SubmitHandler = this.submitHandler, order: OrderRequest? = TEST_ORDER, + addressLookupDelegate: AddressLookupDelegate = this.addressLookupDelegate ): DefaultCardDelegate { val componentParams = CardComponentParamsMapper( + commonComponentParamsMapper = CommonComponentParamsMapper(), installmentsParamsMapper = InstallmentsParamsMapper(), - overrideComponentParams = null, - overrideSessionParams = null - ).mapToParamsDefault(configuration, paymentMethod, null) + ).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = paymentMethod, + ) return DefaultCardDelegate( observerRepository = PaymentObserverRepository(), @@ -1189,32 +1237,43 @@ internal class DefaultCardDelegateTest( genericEncryptor = genericEncryptor, analyticsRepository = analyticsRepository, submitHandler = submitHandler, + addressLookupDelegate = addressLookupDelegate, ) } - private fun getDefaultCardConfigurationBuilder(shopperLocale: Locale = Locale.US): CardConfiguration.Builder { - return CardConfiguration.Builder(shopperLocale, Environment.TEST, TEST_CLIENT_KEY) - .setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD) + private fun createCheckoutConfiguration( + shopperLocale: Locale = Locale.US, + amount: Amount? = null, + configuration: CardConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + card { + setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD) + apply(configuration) + } } - private fun getCustomCardConfigurationBuilder(): CardConfiguration.Builder { - return CardConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY) - .setHideCvc(true) - .setShopperReference("shopper_android") - .setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) - .setInstallmentConfigurations( - InstallmentConfiguration( - InstallmentOptions.DefaultInstallmentOptions( - maxInstallments = 3, - includeRevolving = true - ) - ) - ) - .setHolderNameRequired(true) - .setAddressConfiguration(AddressConfiguration.FullAddress()) - .setKcpAuthVisibility(KCPAuthVisibility.SHOW) - .setShowStorePaymentField(false) - .setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.AMERICAN_EXPRESS) + private fun getCustomCardConfiguration() = createCheckoutConfiguration { + setHideCvc(true) + setShopperReference("shopper_android") + setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + setInstallmentConfigurations( + InstallmentConfiguration( + InstallmentOptions.DefaultInstallmentOptions( + maxInstallments = 3, + includeRevolving = true, + ), + ), + ) + setHolderNameRequired(true) + setAddressConfiguration(AddressConfiguration.FullAddress()) + setKcpAuthVisibility(KCPAuthVisibility.SHOW) + setShowStorePaymentField(false) + setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.AMERICAN_EXPRESS) } @Suppress("LongParameterList") @@ -1245,13 +1304,13 @@ internal class DefaultCardDelegateTest( CardListItem( CardBrand(cardType = CardType.VISA), true, - Environment.TEST + Environment.TEST, ), CardListItem( CardBrand(cardType = CardType.MASTERCARD), false, - Environment.TEST - ) + Environment.TEST, + ), ), isCardListVisible: Boolean = true ): CardOutputData { @@ -1278,7 +1337,7 @@ internal class DefaultCardDelegateTest( cardBrands = cardBrands, isDualBranded = isDualBranded, kcpBirthDateOrTaxNumberHint = kcpBirthDateOrTaxNumberHint, - isCardListVisible = isCardListVisible + isCardListVisible = isCardListVisible, ) } @@ -1330,7 +1389,7 @@ internal class DefaultCardDelegateTest( country = country, isOptional = isOptional, countryOptions = countryOptions, - stateOptions = stateOptions + stateOptions = stateOptions, ) } @@ -1357,7 +1416,6 @@ internal class DefaultCardDelegateTest( arguments(Amount("EUR", 100), Amount("EUR", 100)), arguments(Amount("USD", 0), Amount("USD", 0)), arguments(null, null), - arguments(null, null), ) } } diff --git a/card/src/test/java/com/adyen/checkout/card/internal/ui/StoredCardDelegateTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/ui/StoredCardDelegateTest.kt index e52031d190..947e81d264 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/ui/StoredCardDelegateTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/ui/StoredCardDelegateTest.kt @@ -19,6 +19,7 @@ import com.adyen.checkout.card.InstallmentOptions import com.adyen.checkout.card.KCPAuthVisibility import com.adyen.checkout.card.R import com.adyen.checkout.card.SocialSecurityNumberVisibility +import com.adyen.checkout.card.card import com.adyen.checkout.card.internal.data.model.Brand import com.adyen.checkout.card.internal.data.model.DetectedCardType import com.adyen.checkout.card.internal.ui.model.CardComponentParamsMapper @@ -29,6 +30,7 @@ import com.adyen.checkout.card.internal.ui.model.InputFieldUIState import com.adyen.checkout.card.internal.ui.model.InstallmentsParamsMapper import com.adyen.checkout.card.internal.ui.view.InstallmentModel import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.StoredPaymentMethod @@ -36,6 +38,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyRepository import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.paymentmethod.CardPaymentMethod @@ -45,7 +49,6 @@ import com.adyen.checkout.cse.internal.test.TestCardEncryptor import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import com.adyen.checkout.ui.core.internal.util.AddressValidationUtils import kotlinx.coroutines.CoroutineScope @@ -107,9 +110,9 @@ internal class StoredCardDelegateTest( @Test fun `when component is initialized with cvc shown, then view flow emits StoredCardView`() = runTest { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setHideCvcStoredCard(false) - .build() + configuration = createCheckoutConfiguration { + setHideCvcStoredCard(false) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.viewFlow.test { @@ -120,9 +123,9 @@ internal class StoredCardDelegateTest( @Test fun `when component is initialized with cvc hidden, then view flow emits null`() = runTest { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setHideCvcStoredCard(true) - .build() + configuration = createCheckoutConfiguration { + setHideCvcStoredCard(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.viewFlow.test { @@ -158,7 +161,7 @@ internal class StoredCardDelegateTest( @Test fun `input is empty with custom config, then output data should be invalid`() = runTest { delegate = createCardDelegate( - configuration = getCustomCardConfigurationBuilder().build() + configuration = getCustomCardConfiguration(), ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -201,9 +204,9 @@ internal class StoredCardDelegateTest( @Test fun `security code is empty with hide cvc stored config, then output data should be valid`() = runTest { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setHideCvcStoredCard(true) - .build() + configuration = createCheckoutConfiguration { + setHideCvcStoredCard(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -222,7 +225,7 @@ internal class StoredCardDelegateTest( delegate = createCardDelegate( storedPaymentMethod = getStoredPaymentMethod( brand = CardType.BCMC.txVariant, - ) + ), ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -279,9 +282,9 @@ internal class StoredCardDelegateTest( createOutputData( securityCodeState = FieldState( "12", - Validation.Invalid(R.string.checkout_security_code_not_valid) - ) - ) + Validation.Invalid(R.string.checkout_security_code_not_valid), + ), + ), ) val componentState = expectMostRecentItem() @@ -348,9 +351,7 @@ internal class StoredCardDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultCardConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createCardDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -375,9 +376,9 @@ internal class StoredCardDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -387,9 +388,9 @@ internal class StoredCardDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createCardDelegate( - configuration = getDefaultCardConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -447,17 +448,22 @@ internal class StoredCardDelegateTest( private fun createCardDelegate( publicKeyRepository: PublicKeyRepository = this.publicKeyRepository, cardEncryptor: BaseCardEncryptor = this.cardEncryptor, - configuration: CardConfiguration = getDefaultCardConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), storedPaymentMethod: StoredPaymentMethod = getStoredPaymentMethod(), analyticsRepository: AnalyticsRepository = this.analyticsRepository, submitHandler: SubmitHandler = this.submitHandler, order: OrderRequest? = TEST_ORDER, ): StoredCardDelegate { val componentParams = CardComponentParamsMapper( + commonComponentParamsMapper = CommonComponentParamsMapper(), installmentsParamsMapper = InstallmentsParamsMapper(), - overrideComponentParams = null, - overrideSessionParams = null - ).mapToParamsStored(configuration, null) + ).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + StoredPaymentMethod(), + ) return StoredCardDelegate( observerRepository = PaymentObserverRepository(), @@ -490,27 +496,34 @@ internal class StoredCardDelegateTest( ) } - private fun getDefaultCardConfigurationBuilder(): CardConfiguration.Builder { - return CardConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: CardConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + card(configuration) } - private fun getCustomCardConfigurationBuilder(): CardConfiguration.Builder { - return CardConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY) - .setHideCvc(true) - .setShopperReference("shopper_android") - .setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) - .setInstallmentConfigurations( - InstallmentConfiguration( - InstallmentOptions.DefaultInstallmentOptions( - maxInstallments = 3, - includeRevolving = true - ) - ) - ) - .setHolderNameRequired(true) - .setAddressConfiguration(AddressConfiguration.FullAddress()) - .setKcpAuthVisibility(KCPAuthVisibility.SHOW) - .setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.AMERICAN_EXPRESS) + private fun getCustomCardConfiguration() = createCheckoutConfiguration { + setHideCvc(true) + setShopperReference("shopper_android") + setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + setInstallmentConfigurations( + InstallmentConfiguration( + InstallmentOptions.DefaultInstallmentOptions( + maxInstallments = 3, + includeRevolving = true, + ), + ), + ) + setHolderNameRequired(true) + setAddressConfiguration(AddressConfiguration.FullAddress()) + setKcpAuthVisibility(KCPAuthVisibility.SHOW) + setSupportedCardTypes(CardType.VISA, CardType.MASTERCARD, CardType.AMERICAN_EXPRESS) } @Suppress("LongParameterList") @@ -522,7 +535,9 @@ internal class StoredCardDelegateTest( socialSecurityNumberState: FieldState = FieldState("", Validation.Valid), kcpBirthDateOrTaxNumberState: FieldState = FieldState("", Validation.Valid), kcpCardPasswordState: FieldState = FieldState("", Validation.Valid), - addressState: AddressOutputData = AddressValidationUtils.makeValidEmptyAddressOutput(AddressInputModel()), + addressState: AddressOutputData = AddressValidationUtils.makeValidEmptyAddressOutput( + AddressInputModel(), + ), installmentState: FieldState = FieldState(null, Validation.Valid), shouldStorePaymentMethod: Boolean = false, cvcUIState: InputFieldUIState = InputFieldUIState.REQUIRED, @@ -560,7 +575,7 @@ internal class StoredCardDelegateTest( cardBrands = cardBrands, isDualBranded = false, kcpBirthDateOrTaxNumberHint = null, - isCardListVisible = isCardListVisible + isCardListVisible = isCardListVisible, ) } diff --git a/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt b/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt index 73da3b7145..aca0b1a417 100644 --- a/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt +++ b/card/src/test/java/com/adyen/checkout/card/internal/ui/model/CardComponentParamsMapperTest.kt @@ -16,11 +16,17 @@ import com.adyen.checkout.card.InstallmentConfiguration import com.adyen.checkout.card.InstallmentOptions import com.adyen.checkout.card.KCPAuthVisibility import com.adyen.checkout.card.SocialSecurityNumberVisibility +import com.adyen.checkout.card.card import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentOptionsParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams @@ -35,15 +41,16 @@ import java.util.Locale internal class CardComponentParamsMapperTest { + private val cardComponentParamsMapper = CardComponentParamsMapper( + CommonComponentParamsMapper(), + InstallmentsParamsMapper(), + ) + @Test - fun `when parent configuration is null and custom card configuration fields are null then all fields should match`() { - val cardConfiguration = getCardConfigurationBuilder().build() + fun `when drop-in override params are null and custom card configuration fields are null then all fields should match`() { + val configuration = createCheckoutConfiguration() - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - null - ) + val params = cardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, PaymentMethod()) val expected = getCardComponentParams() @@ -51,50 +58,54 @@ internal class CardComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom card configuration fields are set then all fields should match`() { + fun `when drop-in override params are null and custom card configuration fields are set then all fields should match`() { val shopperReference = "SHOPPER_REFERENCE_1" val installmentConfiguration = InstallmentConfiguration( InstallmentOptions.DefaultInstallmentOptions( maxInstallments = 3, - includeRevolving = true - ) + includeRevolving = true, + ), ) val expectedInstallmentParams = InstallmentParams( defaultOptions = InstallmentOptionParams.DefaultInstallmentOptions( values = listOf(2, 3), - includeRevolving = true + includeRevolving = true, ), - shopperLocale = Locale.FRANCE + shopperLocale = Locale.FRANCE, ) val addressConfiguration = AddressConfiguration.FullAddress(supportedCountryCodes = listOf("CA", "GB")) val expectedAddressParams = AddressParams.FullAddress( supportedCountryCodes = addressConfiguration.supportedCountryCodes, - addressFieldPolicy = AddressFieldPolicyParams.Required + addressFieldPolicy = AddressFieldPolicyParams.Required, ) - val cardConfiguration = CardConfiguration.Builder( + val configuration = CheckoutConfiguration( shopperLocale = Locale.FRANCE, environment = Environment.APSE, - clientKey = TEST_CLIENT_KEY_2 - ) - .setHolderNameRequired(true) - .setSupportedCardTypes(CardType.DINERS, CardType.MAESTRO) - .setShopperReference(shopperReference) - .setShowStorePaymentField(false) - .setHideCvc(true) - .setHideCvcStoredCard(true) - .setSubmitButtonVisible(false) - .setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) - .setKcpAuthVisibility(KCPAuthVisibility.SHOW) - .setInstallmentConfigurations(installmentConfiguration) - .setAddressConfiguration(addressConfiguration) - .build() - - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - null + clientKey = TEST_CLIENT_KEY_2, + ) { + card { + setHolderNameRequired(true) + setSupportedCardTypes(CardType.DINERS, CardType.MAESTRO) + setShopperReference(shopperReference) + setShowStorePaymentField(false) + setHideCvc(true) + setHideCvcStoredCard(true) + setSubmitButtonVisible(false) + setSocialSecurityNumberVisibility(SocialSecurityNumberVisibility.SHOW) + setKcpAuthVisibility(KCPAuthVisibility.SHOW) + setInstallmentConfigurations(installmentConfiguration) + setAddressConfiguration(addressConfiguration) + } + } + + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( @@ -104,7 +115,7 @@ internal class CardComponentParamsMapperTest { isHolderNameRequired = true, supportedCardBrands = listOf( CardBrand(cardType = CardType.DINERS), - CardBrand(cardType = CardType.MAESTRO) + CardBrand(cardType = CardType.MAESTRO), ), shopperReference = shopperReference, isStorePaymentFieldVisible = false, @@ -114,34 +125,37 @@ internal class CardComponentParamsMapperTest { socialSecurityNumberVisibility = SocialSecurityNumberVisibility.SHOW, kcpAuthVisibility = KCPAuthVisibility.SHOW, installmentParams = expectedInstallmentParams, - addressParams = expectedAddressParams + addressParams = expectedAddressParams, ) assertEquals(expected, params) } @Test - fun `when parent configuration is set then parent configuration fields should override card configuration fields`() { - val cardConfiguration = getCardConfigurationBuilder().build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override card configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "CAD", - value = 1235_00L - ) - ) - - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), overrideParams, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - null + value = 1235_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + card { + setAmount(Amount("USD", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } + + val dropInOverrideParams = DropInOverrideParams(Amount("EUR", 123L), null) + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( @@ -151,9 +165,9 @@ internal class CardComponentParamsMapperTest { analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "CAD", - value = 1235_00L - ) + currency = "EUR", + value = 123L, + ), ) assertEquals(expected, params) @@ -161,25 +175,21 @@ internal class CardComponentParamsMapperTest { @Test fun `when supported card types are set in the card configuration then they should be used in the params`() { - val cardConfiguration = getCardConfigurationBuilder() - .setSupportedCardTypes(CardType.MAESTRO, CardType.BCMC) - .build() + val configuration = createCheckoutConfiguration { + setSupportedCardTypes(CardType.MAESTRO, CardType.BCMC) + } val paymentMethod = PaymentMethod( brands = listOf( CardType.VISA.txVariant, - CardType.MASTERCARD.txVariant - ) + CardType.MASTERCARD.txVariant, + ), ) - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - paymentMethod, - null - ) + val params = cardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, paymentMethod) val expected = getCardComponentParams( - supportedCardBrands = listOf(CardBrand(cardType = CardType.MAESTRO), CardBrand(cardType = CardType.BCMC)) + supportedCardBrands = listOf(CardBrand(cardType = CardType.MAESTRO), CardBrand(cardType = CardType.BCMC)), ) assertEquals(expected, params) @@ -187,23 +197,18 @@ internal class CardComponentParamsMapperTest { @Test fun `when there are any restricted card brand in payment method,they are removed from the params`() { - val cardConfiguration = getCardConfigurationBuilder().build() - val paymentMethod = - PaymentMethod( - brands = listOf( - RestrictedCardType.NYCE.txVariant, - CardType.MASTERCARD.txVariant - ) - ) - - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - paymentMethod, - null + val configuration = createCheckoutConfiguration() + val paymentMethod = PaymentMethod( + brands = listOf( + RestrictedCardType.NYCE.txVariant, + CardType.MASTERCARD.txVariant, + ), ) + val params = cardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, paymentMethod) + val expected = getCardComponentParams( - supportedCardBrands = listOf(CardBrand(cardType = CardType.MASTERCARD)) + supportedCardBrands = listOf(CardBrand(cardType = CardType.MASTERCARD)), ) assertEquals(expected, params) @@ -211,27 +216,22 @@ internal class CardComponentParamsMapperTest { @Test fun `when supported card types are not set in the card configuration and payment method brands exist then brands should be used in the params`() { - val cardConfiguration = getCardConfigurationBuilder() - .build() + val configuration = createCheckoutConfiguration() val paymentMethod = PaymentMethod( brands = listOf( CardType.VISA.txVariant, - CardType.MASTERCARD.txVariant - ) + CardType.MASTERCARD.txVariant, + ), ) - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - paymentMethod, - null - ) + val params = cardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, paymentMethod) val expected = getCardComponentParams( supportedCardBrands = listOf( CardBrand(cardType = CardType.VISA), - CardBrand(cardType = CardType.MASTERCARD) - ) + CardBrand(cardType = CardType.MASTERCARD), + ), ) assertEquals(expected, params) @@ -239,17 +239,12 @@ internal class CardComponentParamsMapperTest { @Test fun `when supported card types are not set in the card configuration and payment method brands do not exist then the default card types should be used in the params`() { - val cardConfiguration = getCardConfigurationBuilder() - .build() + val configuration = createCheckoutConfiguration() - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - null - ) + val params = cardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, PaymentMethod()) val expected = getCardComponentParams( - supportedCardBrands = CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST + supportedCardBrands = CardConfiguration.DEFAULT_SUPPORTED_CARDS_LIST, ) assertEquals(expected, params) @@ -263,23 +258,24 @@ internal class CardComponentParamsMapperTest { sessionsValue: Boolean?, expectedValue: Boolean ) { - val cardConfiguration = getCardConfigurationBuilder() - .setShowStorePaymentField(configurationValue) - .build() - - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - sessionParams = SessionParams( - enableStoreDetails = sessionsValue, - installmentConfiguration = null, - amount = null, - returnUrl = "", - ) + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(configurationValue) + } + + val sessionParams = createSessionParams( + enableStoreDetails = sessionsValue, + ) + + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( - isStorePaymentFieldVisible = expectedValue + isStorePaymentFieldVisible = expectedValue, ) assertEquals(expected, params) @@ -287,30 +283,29 @@ internal class CardComponentParamsMapperTest { @Test fun `installmentParams should be null if set as null in sessions`() { - val cardConfiguration = getCardConfigurationBuilder() - .setInstallmentConfigurations( + val configuration = createCheckoutConfiguration { + setInstallmentConfigurations( InstallmentConfiguration( InstallmentOptions.DefaultInstallmentOptions( maxInstallments = 3, - includeRevolving = true - ) - ) - ) - .build() - - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = null, - returnUrl = "", + includeRevolving = true, + ), + ), ) + } + + val sessionParams = createSessionParams() + + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( - installmentParams = null + installmentParams = null, ) assertEquals(expected, params) @@ -322,43 +317,47 @@ internal class CardComponentParamsMapperTest { "card" to SessionInstallmentOptionsParams( plans = listOf("regular"), preselectedValue = 2, - values = listOf(2) - ) + values = listOf(2), + ), ) val installmentConfiguration = SessionInstallmentConfiguration( installmentOptions = installmentOptions, - showInstallmentAmount = false + showInstallmentAmount = false, ) - val cardConfiguration = getCardConfigurationBuilder() - .setInstallmentConfigurations( + val configuration = createCheckoutConfiguration { + setInstallmentConfigurations( InstallmentConfiguration( defaultOptions = InstallmentOptions.DefaultInstallmentOptions( maxInstallments = 3, - includeRevolving = true - ) - ) + includeRevolving = true, + ), + ), ) - .build() + } - val mapper = InstallmentsParamsMapper() + val installmentsParamsMapper = InstallmentsParamsMapper() + val sessionParams = createSessionParams( + installmentConfiguration = installmentConfiguration, + ) + val cardComponentParamsMapper = CardComponentParamsMapper( + CommonComponentParamsMapper(), + installmentsParamsMapper, + ) - val params = CardComponentParamsMapper(mapper, null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = installmentConfiguration, - amount = null, - returnUrl = "", - ) + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( - installmentParams = mapper.mapToInstallmentParams( + installmentParams = installmentsParamsMapper.mapToInstallmentParams( installmentConfiguration = installmentConfiguration, - amount = cardConfiguration.amount, - shopperLocale = cardConfiguration.shopperLocale - ) + amount = configuration.amount, + shopperLocale = configuration.shopperLocale ?: DEVICE_LOCALE, + ), ) assertEquals(expected, params) @@ -369,27 +368,33 @@ internal class CardComponentParamsMapperTest { val installmentConfiguration = InstallmentConfiguration( InstallmentOptions.DefaultInstallmentOptions( maxInstallments = 3, - includeRevolving = true - ) + includeRevolving = true, + ), ) - val cardConfiguration = getCardConfigurationBuilder() - .setInstallmentConfigurations(installmentConfiguration) - .build() + val configuration = createCheckoutConfiguration { + setInstallmentConfigurations(installmentConfiguration) + } - val mapper = InstallmentsParamsMapper() + val installmentsParamsMapper = InstallmentsParamsMapper() + val cardComponentParamsMapper = CardComponentParamsMapper( + CommonComponentParamsMapper(), + installmentsParamsMapper, + ) - val params = CardComponentParamsMapper(mapper, null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - sessionParams = null, + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( - installmentParams = mapper.mapToInstallmentParams( + installmentParams = installmentsParamsMapper.mapToInstallmentParams( installmentConfiguration = installmentConfiguration, - amount = cardConfiguration.amount, - shopperLocale = cardConfiguration.shopperLocale - ) + amount = configuration.amount, + shopperLocale = configuration.shopperLocale ?: DEVICE_LOCALE, + ), ) assertEquals(expected, params) @@ -397,16 +402,12 @@ internal class CardComponentParamsMapperTest { @Test fun `installmentParams should be null if not set in configuration and there is no session`() { - val cardConfiguration = getCardConfigurationBuilder().build() + val configuration = createCheckoutConfiguration() - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), null, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - sessionParams = null, - ) + val params = cardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null, PaymentMethod()) val expected = getCardComponentParams( - installmentParams = null + installmentParams = null, ) assertEquals(expected, params) @@ -414,47 +415,127 @@ internal class CardComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val cardConfiguration = getCardConfigurationBuilder() - .setAmount(configurationValue) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getCardComponentParams(amount = it) } - - val params = CardComponentParamsMapper(InstallmentsParamsMapper(), overrideParams, null).mapToParamsDefault( - cardConfiguration, - PaymentMethod(), - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ) + val configuration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), ) val expected = getCardComponentParams( - amount = expectedValue + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, ) assertEquals(expected, params) } - private fun getCardConfigurationBuilder() = CardConfiguration.Builder( - shopperLocale = Locale.US, + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), + ) + + val expected = getCardComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = cardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), + ) + + val expected = getCardComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: CardConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + card(configuration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, ) @Suppress("LongParameterList") private fun getCardComponentParams( - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -472,11 +553,14 @@ internal class CardComponentParamsMapperTest { cvcVisibility: CVCVisibility = CVCVisibility.ALWAYS_SHOW, storedCVCVisibility: StoredCVCVisibility = StoredCVCVisibility.SHOW ) = CardComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), isHolderNameRequired = isHolderNameRequired, isSubmitButtonVisible = isSubmitButtonVisible, supportedCardBrands = supportedCardBrands, @@ -486,7 +570,6 @@ internal class CardComponentParamsMapperTest { kcpAuthVisibility = kcpAuthVisibility, installmentParams = installmentParams, addressParams = addressParams, - amount = amount, cvcVisibility = cvcVisibility, storedCVCVisibility = storedCVCVisibility, ) @@ -494,6 +577,7 @@ internal class CardComponentParamsMapperTest { companion object { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun enableStoreDetailsSource() = listOf( @@ -513,5 +597,14 @@ internal class CardComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt index d382c9906f..e7024a199c 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt @@ -25,8 +25,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import com.adyen.checkout.ui.core.internal.ui.ViewableComponent @@ -76,7 +76,7 @@ class CashAppPayComponent internal constructor( override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? DefaultCashAppPayDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun isConfirmationRequired(): Boolean = @@ -84,19 +84,18 @@ class CashAppPayComponent internal constructor( override fun submit() { (delegate as? ButtonDelegate)?.onSubmit() - ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } cashAppPayDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = CashAppPayComponentProvider() diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt index 45e04a229b..30ce6e4fb0 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt @@ -13,9 +13,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -27,7 +30,7 @@ import java.util.Locale class CashAppPayConfiguration @Suppress("LongParameterList") private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -50,6 +53,22 @@ private constructor( private var showStorePaymentField: Boolean? = null private var storePaymentMethod: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -57,10 +76,11 @@ private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** @@ -73,7 +93,7 @@ private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -92,6 +112,10 @@ private constructor( * * Sets the required return URL that Cash App Pay will redirect to at the end of the transaction. * + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * * @param returnUrl The Cash App Pay environment. */ fun setReturnUrl(returnUrl: String): Builder { @@ -104,8 +128,9 @@ private constructor( * * Default is true. * - * When using `sessions` show store payment field will be ignored and replaced with the value sent to - * `/sessions` call. + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. * * @param showStorePaymentField [Boolean] * @return [CashAppPayConfiguration.Builder] @@ -158,3 +183,38 @@ private constructor( ) } } + +fun CheckoutConfiguration.cashAppPay( + configuration: @CheckoutConfigurationMarker CashAppPayConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = CashAppPayConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.CASH_APP_PAY, config) + return this +} + +fun CheckoutConfiguration.getCashAppPayConfiguration(): CashAppPayConfiguration? { + return getConfiguration(PaymentMethodTypes.CASH_APP_PAY) +} + +internal fun CashAppPayConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.CASH_APP_PAY, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/provider/CashAppPayComponentProvider.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/provider/CashAppPayComponentProvider.kt index 5dbc76ef5a..3979798d38 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/provider/CashAppPayComponentProvider.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/provider/CashAppPayComponentProvider.kt @@ -11,6 +11,7 @@ package com.adyen.checkout.cashapppay.internal.provider import android.app.Application import androidx.annotation.RestrictTo import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner @@ -20,13 +21,18 @@ import com.adyen.checkout.action.core.internal.provider.GenericActionComponentPr import com.adyen.checkout.cashapppay.CashAppPayComponent import com.adyen.checkout.cashapppay.CashAppPayComponentState import com.adyen.checkout.cashapppay.CashAppPayConfiguration +import com.adyen.checkout.cashapppay.internal.ui.CashAppPayDelegate import com.adyen.checkout.cashapppay.internal.ui.DefaultCashAppPayDelegate import com.adyen.checkout.cashapppay.internal.ui.StoredCashAppPayDelegate +import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParams import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParamsMapper +import com.adyen.checkout.cashapppay.toCheckoutConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.DefaultComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper @@ -36,12 +42,14 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.provider.StoredPaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException +import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -54,46 +62,45 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionStoredPaymentCo import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler +@Suppress("TooManyFunctions") class CashAppPayComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< CashAppPayComponent, CashAppPayConfiguration, CashAppPayComponentState, - ComponentCallback + ComponentCallback, >, StoredPaymentComponentProvider< CashAppPayComponent, CashAppPayConfiguration, CashAppPayComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< CashAppPayComponent, CashAppPayConfiguration, CashAppPayComponentState, - SessionComponentCallback + SessionComponentCallback, >, SessionStoredPaymentComponentProvider< CashAppPayComponent, CashAppPayConfiguration, CashAppPayComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = CashAppPayComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: CashAppPayConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -102,7 +109,15 @@ constructor( assertSupported(paymentMethod) val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null, paymentMethod) + val componentParams = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = paymentMethod, + context = application, + ) + val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( application = application, @@ -110,7 +125,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -125,16 +140,11 @@ constructor( cashAppPayFactory = CashAppPayFactory, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + createComponent( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, - ) - - CashAppPayComponent( - cashAppPayDelegate = cashAppPayDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate), + delegate = cashAppPayDelegate, componentEventHandler = DefaultComponentEventHandler(), ) } @@ -151,17 +161,49 @@ constructor( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, - storedPaymentMethod: StoredPaymentMethod, + paymentMethod: PaymentMethod, configuration: CashAppPayConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, key: String? + ): CashAppPayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? ): CashAppPayComponent { assertSupported(storedPaymentMethod) val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null, storedPaymentMethod) + val componentParams = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + storedPaymentMethod = storedPaymentMethod, + context = application, + ) + val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( application = application, @@ -169,7 +211,7 @@ constructor( storedPaymentMethod = storedPaymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -182,16 +224,11 @@ constructor( componentParams = componentParams, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + createComponent( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, - ) - - CashAppPayComponent( - cashAppPayDelegate = cashAppPayDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate), + delegate = cashAppPayDelegate, componentEventHandler = DefaultComponentEventHandler(), ) } @@ -204,6 +241,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + configuration: CashAppPayConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): CashAppPayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -211,7 +272,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: CashAppPayConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -219,10 +280,13 @@ constructor( assertSupported(paymentMethod) val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), paymentMethod = paymentMethod, + context = application, ) val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) @@ -235,7 +299,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -250,35 +314,18 @@ constructor( cashAppPayFactory = CashAppPayFactory, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, - savedStateHandle = savedStateHandle, - application = application, - ) - - val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + val sessionComponentEventHandler = createSessionComponentEventHandler( savedStateHandle = savedStateHandle, checkoutSession = checkoutSession, + httpClient = httpClient, + componentParams = componentParams, ) - val sessionInteractor = SessionInteractor( - sessionRepository = SessionRepository( - sessionService = SessionService(httpClient), - clientKey = componentParams.clientKey, - ), - sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false - ) - - val sessionComponentEventHandler = SessionComponentEventHandler( - sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, - ) - - CashAppPayComponent( - cashAppPayDelegate = cashAppPayDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate), + createComponent( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + delegate = cashAppPayDelegate, componentEventHandler = sessionComponentEventHandler, ) } @@ -291,6 +338,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: CashAppPayConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): CashAppPayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -298,7 +369,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, storedPaymentMethod: StoredPaymentMethod, - configuration: CashAppPayConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -306,10 +377,13 @@ constructor( assertSupported(storedPaymentMethod) val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), - paymentMethod = storedPaymentMethod, + val componentParams = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + storedPaymentMethod = storedPaymentMethod, + context = application, ) val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) @@ -322,7 +396,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -335,35 +409,18 @@ constructor( componentParams = componentParams, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, - savedStateHandle = savedStateHandle, - application = application, - ) - - val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + val sessionComponentEventHandler = createSessionComponentEventHandler( savedStateHandle = savedStateHandle, checkoutSession = checkoutSession, + httpClient = httpClient, + componentParams = componentParams, ) - val sessionInteractor = SessionInteractor( - sessionRepository = SessionRepository( - sessionService = SessionService(httpClient), - clientKey = componentParams.clientKey, - ), - sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false - ) - - val sessionComponentEventHandler = SessionComponentEventHandler( - sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, - ) - - CashAppPayComponent( - cashAppPayDelegate = cashAppPayDelegate, - genericActionDelegate = genericActionDelegate, - actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate), + createComponent( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + delegate = cashAppPayDelegate, componentEventHandler = sessionComponentEventHandler, ) } @@ -376,6 +433,77 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + configuration: CashAppPayConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): CashAppPayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + + private fun createComponent( + checkoutConfiguration: CheckoutConfiguration, + savedStateHandle: SavedStateHandle, + application: Application, + delegate: CashAppPayDelegate, + componentEventHandler: ComponentEventHandler, + ): CashAppPayComponent { + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, + savedStateHandle = savedStateHandle, + application = application, + ) + + return CashAppPayComponent( + cashAppPayDelegate = delegate, + genericActionDelegate = genericActionDelegate, + actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, delegate), + componentEventHandler = componentEventHandler, + ) + } + + private fun createSessionComponentEventHandler( + savedStateHandle: SavedStateHandle, + checkoutSession: CheckoutSession, + httpClient: HttpClient, + componentParams: CashAppPayComponentParams, + ): SessionComponentEventHandler { + val sessionSavedStateHandleContainer = SessionSavedStateHandleContainer( + savedStateHandle = savedStateHandle, + checkoutSession = checkoutSession, + ) + + val sessionInteractor = SessionInteractor( + sessionRepository = SessionRepository( + sessionService = SessionService(httpClient), + clientKey = componentParams.clientKey, + ), + sessionModel = sessionSavedStateHandleContainer.getSessionModel(), + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, + ) + + return SessionComponentEventHandler( + sessionInteractor = sessionInteractor, + sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegate.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegate.kt index b700e2544b..6979720e40 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegate.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegate.kt @@ -36,10 +36,10 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.CashAppPayPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -64,7 +64,7 @@ constructor( private val order: OrderRequest?, override val componentParams: CashAppPayComponentParams, private val cashAppPayFactory: CashAppPayFactory, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : CashAppPayDelegate, ButtonDelegate, CashAppPayListener { private val inputData = CashAppPayInputData() @@ -111,7 +111,7 @@ constructor( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -128,7 +128,7 @@ constructor( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -155,7 +155,7 @@ constructor( @VisibleForTesting internal fun updateComponentState(outputData: CashAppPayOutputData) { - Logger.v(TAG, "updateComponentState") + adyenLog(AdyenLogLevel.VERBOSE) { "updateComponentState" } val componentState = createComponentState(outputData) _componentStateFlow.tryEmit(componentState) } @@ -185,7 +185,7 @@ constructor( return CashAppPayComponentState( data = paymentComponentData, isInputValid = outputData.isValid, - isReady = true + isReady = true, ) } @@ -205,15 +205,15 @@ constructor( exceptionChannel.trySend( ComponentException( "Cannot launch Cash App Pay, you need to either pass an amount with supported " + - "currency or store the shopper account." - ) + "currency or store the shopper account.", + ), ) return } _viewFlow.tryEmit(PaymentInProgressViewType) - coroutineScope.launch(ioDispatcher) { + coroutineScope.launch(coroutineDispatcher) { cashAppPay.createCustomerRequest(actions, componentParams.returnUrl) } } @@ -259,14 +259,14 @@ constructor( } override fun cashAppPayStateDidChange(newState: CashAppPayState) { - Logger.d(TAG, "CashAppPayState state changed: ${newState::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "CashAppPayState state changed: ${newState::class.simpleName}" } when (newState) { is CashAppPayState.ReadyToAuthorize -> { cashAppPay.authorizeCustomerRequest() } is CashAppPayState.Approved -> { - Logger.i(TAG, "Cash App Pay authorization request approved") + adyenLog(AdyenLogLevel.INFO) { "Cash App Pay authorization request approved" } updateInputData { authorizationData = createAuthorizationData(newState.responseData) } @@ -274,13 +274,13 @@ constructor( } CashAppPayState.Declined -> { - Logger.i(TAG, "Cash App Pay authorization request declined") + adyenLog(AdyenLogLevel.INFO) { "Cash App Pay authorization request declined" } exceptionChannel.trySend(ComponentException("Cash App Pay authorization request declined")) } is CashAppPayState.CashAppPayExceptionState -> { exceptionChannel.trySend( - ComponentException("Cash App Pay has encountered an error", newState.exception) + ComponentException("Cash App Pay has encountered an error", newState.exception), ) } @@ -296,7 +296,7 @@ constructor( CashAppPayOnFileData( grantId = it.id, cashTag = customerResponseData.customerProfile?.cashTag?.toString(), - customerId = customerResponseData.customerProfile?.id + customerId = customerResponseData.customerProfile?.id, ) } @@ -325,8 +325,4 @@ constructor( removeObserver() cashAppPay.unregisterFromStateUpdates() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegate.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegate.kt index 37b1c49e3b..ccf9379d2d 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegate.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegate.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.cashapppay.internal.ui -import android.util.Log import androidx.lifecycle.LifecycleOwner import com.adyen.checkout.cashapppay.CashAppPayComponentState import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParams @@ -22,8 +21,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.CashAppPayPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -60,7 +59,7 @@ internal class StoredCashAppPayDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -83,7 +82,7 @@ internal class StoredCashAppPayDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -92,7 +91,7 @@ internal class StoredCashAppPayDelegate( } override fun updateInputData(update: CashAppPayInputData.() -> Unit) { - Log.w(TAG, "updateInputData should not be called for stored Cash App Pay") + adyenLog(AdyenLogLevel.WARN) { "updateInputData should not be called for stored Cash App Pay" } } private fun createComponentState(): CashAppPayComponentState { @@ -111,7 +110,7 @@ internal class StoredCashAppPayDelegate( return CashAppPayComponentState( data = paymentComponentData, isInputValid = true, - isReady = true + isReady = true, ) } @@ -122,8 +121,4 @@ internal class StoredCashAppPayDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParams.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParams.kt index 19990d3590..694e865ebb 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParams.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParams.kt @@ -9,28 +9,20 @@ package com.adyen.checkout.cashapppay.internal.ui.model import com.adyen.checkout.cashapppay.CashAppPayEnvironment -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment -import java.util.Locale internal data class CashAppPayComponentParams( + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, val cashAppPayEnvironment: CashAppPayEnvironment, val returnUrl: String?, val showStorePaymentField: Boolean, val storePaymentMethod: Boolean, val clientId: String?, val scopeId: String?, -) : ComponentParams, ButtonParams { +) : ComponentParams by commonComponentParams, ButtonParams { fun requireClientId(): String = requireNotNull(clientId) } diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapper.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapper.kt index c57e36fcd3..a51a5d44bc 100644 --- a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapper.kt +++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapper.kt @@ -8,107 +8,154 @@ package com.adyen.checkout.cashapppay.internal.ui.model +import android.content.Context +import com.adyen.checkout.cashapppay.CashAppPayComponent import com.adyen.checkout.cashapppay.CashAppPayConfiguration import com.adyen.checkout.cashapppay.CashAppPayEnvironment +import com.adyen.checkout.cashapppay.getCashAppPayConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException +import java.util.Locale internal class CashAppPayComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { - @Suppress("ThrowsCount") + @Suppress("ThrowsCount", "LongParameterList") fun mapToParams( - configuration: CashAppPayConfiguration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, paymentMethod: PaymentMethod, + context: Context, ): CashAppPayComponentParams { - val params = configuration - .mapToParamsInternal( - clientId = paymentMethod.configuration?.clientId ?: throw ComponentException( - "Cannot launch Cash App Pay, clientId is missing in the payment method object." - ), - scopeId = paymentMethod.configuration?.scopeId ?: throw ComponentException( - "Cannot launch Cash App Pay, scopeId is missing in the payment method object." - ), - ) - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) + val clientId = paymentMethod.configuration?.clientId ?: throw ComponentException( + "Cannot launch Cash App Pay, clientId is missing in the payment method object.", + ) + + val scopeId = paymentMethod.configuration?.scopeId ?: throw ComponentException( + "Cannot launch Cash App Pay, scopeId is missing in the payment method object.", + ) + + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + + val cashAppPayConfiguration = checkoutConfiguration.getCashAppPayConfiguration() + + val params = mapToParamsInternal( + commonComponentParams = commonComponentParamsMapperData.commonComponentParams, + sessionParams = commonComponentParamsMapperData.sessionParams, + cashAppPayConfiguration = cashAppPayConfiguration, + clientId = clientId, + scopeId = scopeId, + context = context, + ) if (params.returnUrl == null) { throw ComponentException( - "Cannot launch Cash App Pay, set the returnUrl in your CashAppPayConfiguration.Builder" + "Cannot launch Cash App Pay, set the returnUrl in your CashAppPayConfiguration.Builder", ) } return params } + @Suppress("LongParameterList") fun mapToParams( - configuration: CashAppPayConfiguration, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + @Suppress("UNUSED_PARAMETER") storedPaymentMethod: StoredPaymentMethod, + context: Context, + ): CashAppPayComponentParams { + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + + val cashAppPayConfiguration = checkoutConfiguration.getCashAppPayConfiguration() + + return mapToParamsInternal( + commonComponentParams = commonComponentParamsMapperData.commonComponentParams, + sessionParams = commonComponentParamsMapperData.sessionParams, + cashAppPayConfiguration = cashAppPayConfiguration, + clientId = null, + scopeId = null, + context = context, + ) + } + + @Suppress("LongParameterList") + private fun mapToParamsInternal( + commonComponentParams: CommonComponentParams, sessionParams: SessionParams?, - @Suppress("UNUSED_PARAMETER") paymentMethod: StoredPaymentMethod, - ): CashAppPayComponentParams = configuration - // clientId and scopeId are not needed in the stored flow. - .mapToParamsInternal(null, null) - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - - private fun CashAppPayConfiguration.mapToParamsInternal( + cashAppPayConfiguration: CashAppPayConfiguration?, clientId: String?, scopeId: String?, - ) = CashAppPayComponentParams( - isSubmitButtonVisible = isSubmitButtonVisible ?: true, - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - cashAppPayEnvironment = getCashAppPayEnvironment(), - returnUrl = returnUrl, - showStorePaymentField = showStorePaymentField ?: true, - storePaymentMethod = storePaymentMethod ?: false, - clientId = clientId, - scopeId = scopeId, - ) - - private fun CashAppPayConfiguration.getCashAppPayEnvironment(): CashAppPayEnvironment { + context: Context, + ): CashAppPayComponentParams { + return CashAppPayComponentParams( + commonComponentParams = commonComponentParams, + isSubmitButtonVisible = cashAppPayConfiguration?.isSubmitButtonVisible ?: true, + cashAppPayEnvironment = getCashAppPayEnvironment( + commonComponentParams.environment, + cashAppPayConfiguration, + ), + returnUrl = getReturnUrl( + sessionParams, + commonComponentParams.isCreatedByDropIn, + cashAppPayConfiguration, + context, + ), + showStorePaymentField = getShowStorePaymentField(sessionParams, cashAppPayConfiguration), + storePaymentMethod = cashAppPayConfiguration?.storePaymentMethod ?: false, + clientId = clientId, + scopeId = scopeId, + ) + } + + private fun getCashAppPayEnvironment( + environment: Environment, + cashAppPayConfiguration: CashAppPayConfiguration? + ): CashAppPayEnvironment { return when { - cashAppPayEnvironment != null -> cashAppPayEnvironment + cashAppPayConfiguration?.cashAppPayEnvironment != null -> cashAppPayConfiguration.cashAppPayEnvironment environment == Environment.TEST -> CashAppPayEnvironment.SANDBOX else -> CashAppPayEnvironment.PRODUCTION } } - private fun CashAppPayComponentParams.override( - overrideComponentParams: ComponentParams?, - ): CashAppPayComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) + private fun getReturnUrl( + sessionParams: SessionParams?, + isCreatedByDropIn: Boolean, + cashAppPayConfiguration: CashAppPayConfiguration?, + context: Context, + ): String? { + return sessionParams?.returnUrl + ?: cashAppPayConfiguration?.returnUrl + // if using drop-in and return url is not set use the return url default value + ?: CashAppPayComponent.getReturnUrl(context).takeIf { isCreatedByDropIn } } - private fun CashAppPayComponentParams.override( + private fun getShowStorePaymentField( sessionParams: SessionParams?, - ): CashAppPayComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, - showStorePaymentField = sessionParams.enableStoreDetails ?: showStorePaymentField, - returnUrl = sessionParams.returnUrl ?: returnUrl - ) + cashAppPayConfiguration: CashAppPayConfiguration?, + ): Boolean { + return sessionParams?.enableStoreDetails ?: cashAppPayConfiguration?.showStorePaymentField ?: true } } diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt index d3b808c46a..3e558eabdc 100644 --- a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt +++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt @@ -18,8 +18,7 @@ import com.adyen.checkout.cashapppay.internal.ui.DefaultCashAppPayDelegate import com.adyen.checkout.cashapppay.internal.ui.StoredCashAppPayDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.test.extensions.test @@ -43,7 +42,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class CashAppPayComponentTest( @Mock private val cashAppPayDelegate: CashAppPayDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -64,8 +63,6 @@ internal class CashAppPayComponentTest( actionHandlingComponent, componentEventHandler, ) - - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayConfigurationTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayConfigurationTest.kt new file mode 100644 index 0000000000..6f56187e5e --- /dev/null +++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayConfigurationTest.kt @@ -0,0 +1,108 @@ +package com.adyen.checkout.cashapppay + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class CashAppPayConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + cashAppPay { + setCashAppPayEnvironment(CashAppPayEnvironment.PRODUCTION) + setReturnUrl("some url") + setShowStorePaymentField(true) + setStorePaymentMethod(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getCashAppPayConfiguration() + + val expected = CashAppPayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setCashAppPayEnvironment(CashAppPayEnvironment.PRODUCTION) + .setReturnUrl("some url") + .setShowStorePaymentField(true) + .setStorePaymentMethod(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.cashAppPayEnvironment, actual?.cashAppPayEnvironment) + assertEquals(expected.returnUrl, actual?.returnUrl) + assertEquals(expected.showStorePaymentField, actual?.showStorePaymentField) + assertEquals(expected.storePaymentMethod, actual?.storePaymentMethod) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = CashAppPayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setCashAppPayEnvironment(CashAppPayEnvironment.PRODUCTION) + .setReturnUrl("some url") + .setShowStorePaymentField(true) + .setStorePaymentMethod(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualCashAppConfig = actual.getCashAppPayConfiguration() + assertEquals(config.shopperLocale, actualCashAppConfig?.shopperLocale) + assertEquals(config.environment, actualCashAppConfig?.environment) + assertEquals(config.clientKey, actualCashAppConfig?.clientKey) + assertEquals(config.amount, actualCashAppConfig?.amount) + assertEquals(config.analyticsConfiguration, actualCashAppConfig?.analyticsConfiguration) + assertEquals(config.cashAppPayEnvironment, actualCashAppConfig?.cashAppPayEnvironment) + assertEquals(config.returnUrl, actualCashAppConfig?.returnUrl) + assertEquals(config.showStorePaymentField, actualCashAppConfig?.showStorePaymentField) + assertEquals(config.storePaymentMethod, actualCashAppConfig?.storePaymentMethod) + assertEquals(config.isSubmitButtonVisible, actualCashAppConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegateTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegateTest.kt index c34af4cf0f..36743232a4 100644 --- a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegateTest.kt +++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegateTest.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.cashapppay.internal.ui +import android.app.Application import app.cash.paykit.core.CashAppPay import app.cash.paykit.core.CashAppPayFactory import app.cash.paykit.core.CashAppPayState @@ -21,18 +22,21 @@ import app.cash.paykit.core.models.sdk.CashAppPayCurrency import app.cash.paykit.core.models.sdk.CashAppPayPaymentAction import com.adyen.checkout.cashapppay.CashAppPayComponentState import com.adyen.checkout.cashapppay.CashAppPayConfiguration +import com.adyen.checkout.cashapppay.cashAppPay import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayAuthorizationData import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParamsMapper import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayOnFileData import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayOneTimeData import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayOutputData import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Configuration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.paymentmethod.CashAppPayPaymentMethod import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException @@ -96,10 +100,9 @@ internal class DefaultCashAppPayDelegateTest( @Test fun `no confirmation is required, then payment should be initiated`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder() - .setAmount(Amount("USD", 10L)) - .setShowStorePaymentField(false) - .build() + createCheckoutConfiguration(Amount("USD", 10L)) { + setShowStorePaymentField(false) + }, ) delegate.initialize(this) @@ -110,9 +113,7 @@ internal class DefaultCashAppPayDelegateTest( @Test fun `when input data changes, then component state is created`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder() - .setAmount(Amount("USD", 10L)) - .build() + createCheckoutConfiguration(Amount("USD", 10L)), ) val testFlow = delegate.componentStateFlow.test(testScheduler) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -121,7 +122,7 @@ internal class DefaultCashAppPayDelegateTest( isStorePaymentSelected = true authorizationData = CashAppPayAuthorizationData( oneTimeData = CashAppPayOneTimeData("grantId", "customerId"), - onFileData = CashAppPayOnFileData("grantId", "cashTag", "customerId") + onFileData = CashAppPayOnFileData("grantId", "cashTag", "customerId"), ) } @@ -141,7 +142,7 @@ internal class DefaultCashAppPayDelegateTest( storePaymentMethod = true, ), isInputValid = true, - isReady = true + isReady = true, ) assertEquals(expected, testFlow.latestValue) } @@ -153,9 +154,9 @@ internal class DefaultCashAppPayDelegateTest( @Test fun `hidden, then it should not show`() { delegate = createDefaultCashAppPayDelegate( - configuration = getConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -164,9 +165,9 @@ internal class DefaultCashAppPayDelegateTest( @Test fun `visible, then it should show`() { delegate = createDefaultCashAppPayDelegate( - configuration = getConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -211,7 +212,7 @@ internal class DefaultCashAppPayDelegateTest( fun `the currency is not supported, then an exception should be propagated`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder().setAmount(Amount("EUR", 100L)).build() + createCheckoutConfiguration(Amount("EUR", 100L)), ) val testFlow = delegate.exceptionFlow.test(testScheduler) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -228,7 +229,7 @@ internal class DefaultCashAppPayDelegateTest( fun `there is any valid action, then the loading view should be shown`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder().setAmount(Amount("USD", 100L)).build() + createCheckoutConfiguration(Amount("USD", 100L)), ) val testFlow = delegate.viewFlow.test(testScheduler) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -242,7 +243,7 @@ internal class DefaultCashAppPayDelegateTest( fun `there is an OneTimeAction, then the Cash App SDK should be called with it`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder().setAmount(Amount("USD", 100L)).build() + createCheckoutConfiguration(Amount("USD", 100L)), ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -254,9 +255,9 @@ internal class DefaultCashAppPayDelegateTest( amount = 100, currency = CashAppPayCurrency.USD, scopeId = TEST_SCOPE_ID, - ) + ), ), - TEST_RETURN_URL + TEST_RETURN_URL, ) } @@ -264,7 +265,7 @@ internal class DefaultCashAppPayDelegateTest( fun `the user doesn't want to store and the component is not configured to store, then there is no OnFileAction`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder().setAmount(Amount("USD", 100L)).build() + createCheckoutConfiguration(Amount("USD", 100L)), ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -276,9 +277,9 @@ internal class DefaultCashAppPayDelegateTest( amount = 100, currency = CashAppPayCurrency.USD, scopeId = TEST_SCOPE_ID, - ) + ), ), - TEST_RETURN_URL + TEST_RETURN_URL, ) } @@ -286,10 +287,9 @@ internal class DefaultCashAppPayDelegateTest( fun `the user wants to store, then the Cash App SDK should be called with an OnFileAction`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder() - .setAmount(Amount("USD", 0L)) - .setShowStorePaymentField(true) - .build() + createCheckoutConfiguration(Amount("USD", 0L)) { + setShowStorePaymentField(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.updateInputData { isStorePaymentSelected = true } @@ -300,7 +300,7 @@ internal class DefaultCashAppPayDelegateTest( listOf( CashAppPayPaymentAction.OnFileAction(scopeId = TEST_SCOPE_ID), ), - TEST_RETURN_URL + TEST_RETURN_URL, ) } @@ -308,11 +308,10 @@ internal class DefaultCashAppPayDelegateTest( fun `the component is configured to store, then the Cash App SDK should be called with an OnFileAction`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder() - .setAmount(Amount("USD", 0L)) - .setShowStorePaymentField(false) - .setStorePaymentMethod(true) - .build() + createCheckoutConfiguration(Amount("USD", 0L)) { + setShowStorePaymentField(false) + setStorePaymentMethod(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -320,7 +319,7 @@ internal class DefaultCashAppPayDelegateTest( listOf( CashAppPayPaymentAction.OnFileAction(scopeId = TEST_SCOPE_ID), ), - TEST_RETURN_URL + TEST_RETURN_URL, ) } @@ -328,11 +327,10 @@ internal class DefaultCashAppPayDelegateTest( fun `the component doesn't require confirmation, then the Cash App SDK should not be called`() = runTest { delegate = createDefaultCashAppPayDelegate( - getConfigurationBuilder() - .setAmount(Amount("USD", 0L)) - .setShowStorePaymentField(false) - .setStorePaymentMethod(true) - .build() + createCheckoutConfiguration(Amount("USD", 0L)) { + setShowStorePaymentField(false) + setStorePaymentMethod(true) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -350,9 +348,7 @@ internal class DefaultCashAppPayDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createDefaultCashAppPayDelegate(configuration = configuration) } val testFlow = delegate.componentStateFlow.test(testScheduler) @@ -387,7 +383,7 @@ internal class DefaultCashAppPayDelegateTest( val mockResponse = mock() whenever(mockResponse.grants) doReturn listOf( createGrant(GrantType.ONE_TIME), - createGrant(GrantType.EXTENDED) + createGrant(GrantType.EXTENDED), ) whenever(mockResponse.customerProfile) doReturn CustomerProfile("customerId", PiiString("cashTag")) delegate.cashAppPayStateDidChange(CashAppPayState.Approved(mockResponse)) @@ -411,7 +407,7 @@ internal class DefaultCashAppPayDelegateTest( val mockResponse = mock() whenever(mockResponse.grants) doReturn listOf( createGrant(GrantType.ONE_TIME), - createGrant(GrantType.EXTENDED) + createGrant(GrantType.EXTENDED), ) whenever(mockResponse.customerProfile) doReturn CustomerProfile("customerId", PiiString("cashTag")) delegate.cashAppPayStateDidChange(CashAppPayState.Approved(mockResponse)) @@ -466,7 +462,7 @@ internal class DefaultCashAppPayDelegateTest( delegate.updateInputData { authorizationData = CashAppPayAuthorizationData( oneTimeData = CashAppPayOneTimeData("grantId", "customerId"), - onFileData = CashAppPayOnFileData("grantId", "cashTag", "customerId") + onFileData = CashAppPayOnFileData("grantId", "cashTag", "customerId"), ) } @@ -475,28 +471,39 @@ internal class DefaultCashAppPayDelegateTest( } private fun createDefaultCashAppPayDelegate( - configuration: CashAppPayConfiguration = getConfigurationBuilder().build() + configuration: CheckoutConfiguration = createCheckoutConfiguration(), ) = DefaultCashAppPayDelegate( submitHandler = submitHandler, analyticsRepository = analyticsRepository, observerRepository = PaymentObserverRepository(), paymentMethod = getPaymentMethod(), order = TEST_ORDER, - componentParams = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, + componentParams = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, paymentMethod = getPaymentMethod(), + context = Application(), ), cashAppPayFactory = cashAppPayFactory, - ioDispatcher = UnconfinedTestDispatcher(), + coroutineDispatcher = UnconfinedTestDispatcher(), ) - private fun getConfigurationBuilder() = CashAppPayConfiguration.Builder( + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: CashAppPayConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( shopperLocale = Locale.US, environment = Environment.TEST, clientKey = "test_qwertyuiopasdfghjklzxcvbnmqwerty", - ) - .setReturnUrl(TEST_RETURN_URL) + amount = amount, + ) { + cashAppPay { + setReturnUrl(TEST_RETURN_URL) + apply(configuration) + } + } private fun getPaymentMethod() = PaymentMethod( configuration = Configuration( diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegateTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegateTest.kt index 3c951b2fa8..370cbd1328 100644 --- a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegateTest.kt +++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegateTest.kt @@ -8,15 +8,18 @@ package com.adyen.checkout.cashapppay.internal.ui +import android.app.Application import com.adyen.checkout.cashapppay.CashAppPayComponentState -import com.adyen.checkout.cashapppay.CashAppPayConfiguration +import com.adyen.checkout.cashapppay.cashAppPay import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParamsMapper import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.paymentmethod.CashAppPayPaymentMethod import com.adyen.checkout.core.Environment import com.adyen.checkout.test.extensions.test @@ -74,9 +77,7 @@ internal class StoredCashAppPayDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createStoredCashAppPayDelegate(configuration = configuration) } val testFlow = delegate.componentStateFlow.test(testScheduler) @@ -112,13 +113,13 @@ internal class StoredCashAppPayDelegateTest( amount = null, ), isInputValid = true, - isReady = true + isReady = true, ) assertEquals(expected, testFlow.latestValue) } private fun createStoredCashAppPayDelegate( - configuration: CashAppPayConfiguration = getConfigurationBuilder().build() + configuration: CheckoutConfiguration = createCheckoutConfiguration() ) = StoredCashAppPayDelegate( analyticsRepository = analyticsRepository, observerRepository = PaymentObserverRepository(), @@ -127,21 +128,31 @@ internal class StoredCashAppPayDelegateTest( type = TEST_PAYMENT_METHOD_TYPE, ), order = TEST_ORDER, - componentParams = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, - paymentMethod = StoredPaymentMethod(), + componentParams = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + storedPaymentMethod = StoredPaymentMethod(), + context = Application(), ), ) - private fun getConfigurationBuilder() = CashAppPayConfiguration.Builder( + private fun createCheckoutConfiguration( + amount: Amount? = null, + ) = CheckoutConfiguration( shopperLocale = Locale.US, environment = Environment.TEST, clientKey = "test_qwertyuiopasdfghjklzxcvbnmqwerty", - ) - .setReturnUrl("test") + amount = amount, + ) { + cashAppPay { + setReturnUrl(TEST_RETURN_URL) + } + } companion object { + private const val TEST_RETURN_URL = "testReturnUrl" private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") private const val TEST_PAYMENT_METHOD_ID = "TEST_PAYMENT_METHOD_ID" private const val TEST_PAYMENT_METHOD_TYPE = "TEST_PAYMENT_METHOD_TYPE" diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt index 2f42582785..878395cb69 100644 --- a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt +++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt @@ -8,15 +8,23 @@ package com.adyen.checkout.cashapppay.internal.ui.model +import android.app.Application import com.adyen.checkout.cashapppay.CashAppPayConfiguration import com.adyen.checkout.cashapppay.CashAppPayEnvironment +import com.adyen.checkout.cashapppay.cashAppPay import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Configuration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException @@ -26,20 +34,26 @@ import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments.arguments import org.junit.jupiter.params.provider.MethodSource +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import java.util.Locale internal class CashAppPayComponentParamsMapperTest { + private val cashAppPayComponentParamsMapper = CashAppPayComponentParamsMapper(CommonComponentParamsMapper()) + @Test - fun `when parent configuration is null and custom configuration fields are null then all fields should match`() { - val configuration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .build() - - val params = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, + fun `when drop-in override params are null and custom configuration fields are null then all fields should match`() { + val configuration = createCheckoutConfiguration() + + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, paymentMethod = getDefaultPaymentMethod(), + context = Application(), ) val expected = getComponentParams() @@ -48,23 +62,28 @@ internal class CashAppPayComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom configuration fields are set then all fields should match`() { - val configuration = CashAppPayConfiguration.Builder( + fun `when drop-in override params are null and custom configuration fields are set then all fields should match`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.FRANCE, environment = Environment.APSE, - clientKey = TEST_CLIENT_KEY_2 - ) - .setCashAppPayEnvironment(CashAppPayEnvironment.PRODUCTION) - .setReturnUrl("https://google.com") - .setShowStorePaymentField(false) - .setStorePaymentMethod(true) - .setSubmitButtonVisible(false) - .build() - - val params = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, + clientKey = TEST_CLIENT_KEY_2, + ) { + cashAppPay { + setCashAppPayEnvironment(CashAppPayEnvironment.PRODUCTION) + setReturnUrl("https://google.com") + setShowStorePaymentField(false) + setStorePaymentMethod(true) + setSubmitButtonVisible(false) + } + } + + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, paymentMethod = getDefaultPaymentMethod(), + context = Application(), ) val expected = getComponentParams( @@ -82,41 +101,46 @@ internal class CashAppPayComponentParamsMapperTest { } @Test - fun `when parent configuration is set then parent configuration fields should override custom configuration fields`() { - val configuration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override custom configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "CAD", - value = 1235_00L - ) - ) + value = 1235_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + cashAppPay { + setReturnUrl(TEST_RETURN_URL) + setAmount(Amount("USD", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } - val params = CashAppPayComponentParamsMapper(overrideParams, null).mapToParams( - configuration = configuration, - sessionParams = null, - paymentMethod = getDefaultPaymentMethod(), - ) + val dropInOverrideParams = DropInOverrideParams(Amount("EUR", 123L), null) + val params = + cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = getDefaultPaymentMethod(), + context = Application(), + ) val expected = getComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, + cashAppPayEnvironment = CashAppPayEnvironment.PRODUCTION, clientKey = TEST_CLIENT_KEY_2, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "CAD", - value = 1235_00L - ) + currency = "EUR", + value = 123L, + ), ) assertEquals(expected, params) @@ -129,24 +153,24 @@ internal class CashAppPayComponentParamsMapperTest { sessionsValue: Boolean?, expectedValue: Boolean ) { - val cardConfiguration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .setShowStorePaymentField(configurationValue) - .build() - - val params = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = cardConfiguration, - sessionParams = SessionParams( - enableStoreDetails = sessionsValue, - installmentConfiguration = null, - amount = null, - returnUrl = TEST_RETURN_URL, - ), + val configuration = createCheckoutConfiguration { + setShowStorePaymentField(configurationValue) + } + val sessionParams = createSessionParams( + enableStoreDetails = sessionsValue, + returnUrl = TEST_RETURN_URL, + ) + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, paymentMethod = getDefaultPaymentMethod(), + context = Application(), ) val expected = getComponentParams( - showStorePaymentField = expectedValue + showStorePaymentField = expectedValue, ) assertEquals(expected, params) @@ -154,34 +178,31 @@ internal class CashAppPayComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val cardConfiguration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .setAmount(configurationValue) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getComponentParams(amount = it) } - - val params = CashAppPayComponentParamsMapper(overrideParams, null).mapToParams( - cardConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = TEST_RETURN_URL, - ), - getDefaultPaymentMethod(), + val configuration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + val sessionParams = createSessionParams( + amount = sessionsValue, + returnUrl = TEST_RETURN_URL, + ) + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + paymentMethod = getDefaultPaymentMethod(), + context = Application(), ) val expected = getComponentParams( - amount = expectedValue + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, ) assertEquals(expected, params) @@ -190,26 +211,45 @@ internal class CashAppPayComponentParamsMapperTest { @Test fun `when returnUrl is not set, then an exception is thrown`() { assertThrows { - val configuration = getConfigurationBuilder() - .build() - - CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, + val configuration = CheckoutConfiguration( + environment = Environment.TEST, + shopperLocale = Locale.US, + clientKey = TEST_CLIENT_KEY_1, + ) { + cashAppPay() + } + + cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, paymentMethod = getDefaultPaymentMethod(), + context = Application(), ) } } @Test fun `when returnUrl is not set and session params are provided, then the return url from sessions should be used`() { - val configuration = getConfigurationBuilder() - .build() - - val params = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = SessionParams(false, null, null, "sessionReturnUrl"), + val configuration = CheckoutConfiguration( + environment = Environment.TEST, + shopperLocale = Locale.US, + clientKey = TEST_CLIENT_KEY_1, + ) { + cashAppPay() + } + val sessionParams = createSessionParams( + enableStoreDetails = false, + returnUrl = "sessionReturnUrl", + ) + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, paymentMethod = getDefaultPaymentMethod(), + context = Application(), ) assertEquals("sessionReturnUrl", params.returnUrl) @@ -218,16 +258,17 @@ internal class CashAppPayComponentParamsMapperTest { @Test fun `when clientId is not available, then an exception is thrown`() { assertThrows { - val configuration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .build() + val configuration = createCheckoutConfiguration() - CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, + cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, paymentMethod = PaymentMethod( - configuration = Configuration(clientId = null, scopeId = TEST_SCOPE_ID) + configuration = Configuration(clientId = null, scopeId = TEST_SCOPE_ID), ), + context = Application(), ) } } @@ -235,30 +276,32 @@ internal class CashAppPayComponentParamsMapperTest { @Test fun `when scopeId is not available, then an exception is thrown`() { assertThrows { - val configuration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .build() + val configuration = createCheckoutConfiguration() - CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, + cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, paymentMethod = PaymentMethod( - configuration = Configuration(clientId = TEST_CLIENT_ID, scopeId = null) + configuration = Configuration(clientId = TEST_CLIENT_ID, scopeId = null), ), + context = Application(), ) } } @Test fun `when StoredPaymentMethod is used, then clientId and scopeId should be null`() { - val configuration = getConfigurationBuilder() - .setReturnUrl(TEST_RETURN_URL) - .build() - - val params = CashAppPayComponentParamsMapper(null, null).mapToParams( - configuration = configuration, - sessionParams = null, - paymentMethod = StoredPaymentMethod(), + val configuration = createCheckoutConfiguration() + + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + storedPaymentMethod = StoredPaymentMethod(), + context = Application(), ) val expected = getComponentParams( @@ -269,9 +312,105 @@ internal class CashAppPayComponentParamsMapperTest { assertEquals(expected, params) } + @ParameterizedTest + @MethodSource("returnUrlSource") + fun `when returnUrl and is created by drop-in, then expect`( + returnUrl: String?, + isCreatedByDropIn: Boolean, + expected: String?, + ) { + val configuration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + ) { + cashAppPay { + returnUrl?.let { setReturnUrl(it) } + } + } + + val mockContext = mock() + whenever(mockContext.packageName) doReturn "com.test.test" + val dropInOverrideParams = if (isCreatedByDropIn) { + DropInOverrideParams(Amount("CAD", 123L), null) + } else { + null + } + val params = + cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + storedPaymentMethod = StoredPaymentMethod(), + context = mockContext, + ) + + assertEquals(expected, params.returnUrl) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + returnUrl = TEST_RETURN_URL, + shopperLocale = sessionsValue, + ) + + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = getDefaultPaymentMethod(), + context = Application(), + ) + + val expected = getComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + returnUrl = TEST_RETURN_URL, + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = cashAppPayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = getDefaultPaymentMethod(), + context = Application(), + ) + + val expected = getComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + cashAppPayEnvironment = CashAppPayEnvironment.PRODUCTION, + ) + + assertEquals(expected, params) + } + @Suppress("LongParameterList") private fun getComponentParams( - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -285,13 +424,15 @@ internal class CashAppPayComponentParamsMapperTest { clientId: String? = TEST_CLIENT_ID, scopeId: String? = TEST_SCOPE_ID, ) = CashAppPayComponentParams( + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), isSubmitButtonVisible = isSubmitButtonVisible, - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, - amount = amount, cashAppPayEnvironment = cashAppPayEnvironment, returnUrl = returnUrl, showStorePaymentField = showStorePaymentField, @@ -300,14 +441,45 @@ internal class CashAppPayComponentParamsMapperTest { scopeId = scopeId, ) - private fun getConfigurationBuilder() = CashAppPayConfiguration.Builder( - shopperLocale = Locale.US, + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: CashAppPayConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + cashAppPay { + setReturnUrl(TEST_RETURN_URL) + apply(configuration) + } + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, ) private fun getDefaultPaymentMethod() = PaymentMethod( - configuration = Configuration(clientId = TEST_CLIENT_ID, scopeId = TEST_SCOPE_ID) + configuration = Configuration(clientId = TEST_CLIENT_ID, scopeId = TEST_SCOPE_ID), ) companion object { @@ -316,6 +488,7 @@ internal class CashAppPayComponentParamsMapperTest { private const val TEST_CLIENT_ID = "test_client_id" private const val TEST_SCOPE_ID = "test_scope_id" private const val TEST_RETURN_URL = "test_return_url" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun enableStoreDetailsSource() = listOf( @@ -335,5 +508,22 @@ internal class CashAppPayComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun returnUrlSource() = listOf( + // Configured URL, isCreatedByDropIn, Expected + arguments(TEST_RETURN_URL, false, TEST_RETURN_URL), + arguments(null, false, null), + arguments(null, true, "adyencheckout://com.test.test"), + ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/checkout-core/build.gradle b/checkout-core/build.gradle index 7fb5bf1e55..4aa77ddbce 100644 --- a/checkout-core/build.gradle +++ b/checkout-core/build.gradle @@ -38,6 +38,7 @@ dependencies { api libraries.androidx.annotation api libraries.kotlinCoroutines implementation libraries.okhttp + api libraries.parcelize //Tests testImplementation testLibraries.json diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogLevel.kt b/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogLevel.kt new file mode 100644 index 0000000000..68465edd38 --- /dev/null +++ b/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogLevel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 2/2/2024. + */ + +package com.adyen.checkout.core + +import android.util.Log + +enum class AdyenLogLevel( + val priority: Int, +) { + VERBOSE(Log.VERBOSE), + DEBUG(Log.DEBUG), + INFO(Log.INFO), + WARN(Log.WARN), + ERROR(Log.ERROR), + ASSERT(Log.ASSERT), + + @Suppress("MagicNumber") + NONE(100), +} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogger.kt b/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogger.kt index 83b8f4ba7b..81f2ea42b3 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogger.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/AdyenLogger.kt @@ -8,17 +8,75 @@ package com.adyen.checkout.core +import android.util.Log +import com.adyen.checkout.core.internal.util.LogcatLogger import com.adyen.checkout.core.internal.util.Logger /** * Utility class to configure the Adyen logger. */ -object AdyenLogger { +interface AdyenLogger { - /** - * Sets the minimum level to be logged. - */ - fun setLogLevel(@Logger.LogLevel logLevel: Int) { - Logger.setLogLevel(logLevel) + fun shouldLog(level: AdyenLogLevel): Boolean + + fun setLogLevel(level: AdyenLogLevel) + + fun log( + level: AdyenLogLevel, + tag: String, + message: String, + throwable: Throwable?, + ) + + companion object { + + @PublishedApi + @Volatile + internal var logger: AdyenLogger = LogcatLogger() + private set + + /** + * Sets the minimum level to be logged. + */ + @Deprecated( + "Logger.LogLevel is deprecated.", + ReplaceWith( + "AdyenLogger.setLogLevel(AdyenLogLevel.)", + "com.adyen.checkout.core.AdyenLogLevel", + "com.adyen.checkout.core.AdyenLogger", + ), + ) + fun setLogLevel(@Logger.LogLevel logLevel: Int) { + val mappedLevel = when (logLevel) { + Log.VERBOSE -> AdyenLogLevel.VERBOSE + Log.DEBUG -> AdyenLogLevel.DEBUG + Log.INFO -> AdyenLogLevel.INFO + Log.WARN -> AdyenLogLevel.WARN + Log.ERROR -> AdyenLogLevel.ERROR + else -> AdyenLogLevel.NONE + } + logger.setLogLevel(mappedLevel) + } + + /** + * Sets the minimum level to be logged. + */ + fun setLogLevel(level: AdyenLogLevel) { + logger.setLogLevel(level) + } + + /** + * Set your own custom instance of [AdyenLogger]. + */ + fun setLogger(logger: AdyenLogger) { + this.logger = logger + } + + /** + * Reset the logger instance back to the default. + */ + fun resetLogger() { + this.logger = LogcatLogger() + } } } diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/PermissionHandlerCallback.kt b/checkout-core/src/main/java/com/adyen/checkout/core/PermissionHandlerCallback.kt new file mode 100644 index 0000000000..2af7297caf --- /dev/null +++ b/checkout-core/src/main/java/com/adyen/checkout/core/PermissionHandlerCallback.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 22/1/2024. + */ + +package com.adyen.checkout.core + +interface PermissionHandlerCallback { + fun onPermissionGranted(requestedPermission: String) + fun onPermissionDenied(requestedPermission: String) + fun onPermissionRequestNotHandled(requestedPermission: String) +} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/exception/PermissionException.kt b/checkout-core/src/main/java/com/adyen/checkout/core/exception/PermissionException.kt index 65ba945505..447709d9fe 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/exception/PermissionException.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/exception/PermissionException.kt @@ -9,9 +9,12 @@ package com.adyen.checkout.core.exception /** - * * This exception indicates that the required runtime permission is not granted. */ +@Deprecated( + message = "This exception is not being used anymore. " + + "To handle runtime permissions, override onPermissionRequest() from ActionComponentCallback." +) class PermissionException( errorMessage: String, val requiredPermission: String diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/api/HttpClientExt.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/api/HttpClientExt.kt index 6652cbbf6c..3b2b5908b4 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/api/HttpClientExt.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/api/HttpClientExt.kt @@ -9,30 +9,28 @@ package com.adyen.checkout.core.internal.data.api import androidx.annotation.RestrictTo +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.HttpException import com.adyen.checkout.core.internal.data.model.ErrorResponseBody import com.adyen.checkout.core.internal.data.model.ModelObject import com.adyen.checkout.core.internal.data.model.ModelUtils import com.adyen.checkout.core.internal.data.model.toStringPretty -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import org.json.JSONArray import org.json.JSONObject -private val TAG = LogUtil.getTag() - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) suspend fun HttpClient.get( path: String, responseSerializer: ModelObject.Serializer, queryParameters: Map = emptyMap(), ): T { - Logger.d(TAG, "GET - $path") + adyenLog(AdyenLogLevel.DEBUG) { "GET - $path" } val result = runAndLogHttpException { get(path, queryParameters) } val resultJson = JSONObject(String(result, Charsets.UTF_8)) - Logger.v(TAG, "response - ${resultJson.toStringPretty()}") + adyenLog(AdyenLogLevel.VERBOSE) { "response - ${resultJson.toStringPretty()}" } return responseSerializer.deserialize(resultJson) } @@ -43,12 +41,12 @@ suspend fun HttpClient.getList( responseSerializer: ModelObject.Serializer, queryParameters: Map = emptyMap(), ): List { - Logger.d(TAG, "GET - $path") + adyenLog(AdyenLogLevel.DEBUG) { "GET - $path" } val result = runAndLogHttpException { get(path, queryParameters) } val resultJson = JSONArray(String(result, Charsets.UTF_8)) - Logger.v(TAG, "response - ${resultJson.toStringPretty()}") + adyenLog(AdyenLogLevel.VERBOSE) { "response - ${resultJson.toStringPretty()}" } return ModelUtils.deserializeOptList(resultJson, responseSerializer).orEmpty() } @@ -61,25 +59,25 @@ suspend fun HttpClient.post( responseSerializer: ModelObject.Serializer, queryParameters: Map = emptyMap(), ): R { - Logger.d(TAG, "POST - $path") + adyenLog(AdyenLogLevel.DEBUG) { "POST - $path" } val requestJson = requestSerializer.serialize(body) - Logger.v(TAG, "request - ${requestJson.toStringPretty()}") + adyenLog(AdyenLogLevel.VERBOSE) { "request - ${requestJson.toStringPretty()}" } val result = runAndLogHttpException { post(path, requestJson.toString(), queryParameters) } val resultJson = JSONObject(String(result, Charsets.UTF_8)) - Logger.v(TAG, "response - ${resultJson.toStringPretty()}") + adyenLog(AdyenLogLevel.VERBOSE) { "response - ${resultJson.toStringPretty()}" } return responseSerializer.deserialize(resultJson) } -private inline fun T.runAndLogHttpException(block: T.() -> R): R { +private inline fun T.runAndLogHttpException(block: T.() -> R): R { return try { block() } catch (httpException: HttpException) { - Logger.e(TAG, "API error - ${httpException.getLogMessage()}") + adyenLog(AdyenLogLevel.ERROR) { "API error - ${httpException.getLogMessage()}" } throw httpException } } diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/ui/PermissionHandler.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/ui/PermissionHandler.kt new file mode 100644 index 0000000000..8ae87a493d --- /dev/null +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/ui/PermissionHandler.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 8/1/2024. + */ + +package com.adyen.checkout.core.internal.ui + +import android.content.Context +import androidx.annotation.RestrictTo +import com.adyen.checkout.core.PermissionHandlerCallback + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun interface PermissionHandler { + fun requestPermission(context: Context, requiredPermission: String, callback: PermissionHandlerCallback) +} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/AdyenLog.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/AdyenLog.kt new file mode 100644 index 0000000000..4f59f2fd06 --- /dev/null +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/AdyenLog.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 2/2/2024. + */ + +package com.adyen.checkout.core.internal.util + +import androidx.annotation.RestrictTo +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.AdyenLogger + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +inline fun Any.adyenLog( + level: AdyenLogLevel, + throwable: Throwable? = null, + log: () -> String, +) { + if (AdyenLogger.logger.shouldLog(level)) { + val fullClassName = this::class.java.name + val outerClassName = fullClassName.substringBefore('$').substringAfterLast('.') + val tag = "CO." + if (outerClassName.isEmpty()) { + fullClassName + } else { + outerClassName.removeSuffix("Kt") + } + + AdyenLogger.logger.log(level, tag, log(), throwable) + } +} + +/** + * This is only meant for top level function where we cannot access `this`. + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +inline fun adyenLog( + level: AdyenLogLevel, + tag: String, + throwable: Throwable? = null, + log: () -> String, +) { + if (AdyenLogger.logger.shouldLog(level)) { + AdyenLogger.logger.log(level, "CO.$tag", log(), throwable) + } +} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/FileDownloader.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/FileDownloader.kt deleted file mode 100644 index 219271f352..0000000000 --- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/FileDownloader.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by atef on 16/11/2022. - */ - -package com.adyen.checkout.core.internal.util - -import android.Manifest -import android.content.ContentValues -import android.content.Context -import android.os.Build -import android.provider.MediaStore -import androidx.annotation.RequiresApi -import androidx.annotation.RequiresPermission -import androidx.annotation.RestrictTo -import com.adyen.checkout.core.exception.CheckoutException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileOutputStream -import java.net.MalformedURLException -import java.net.URL - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -@Suppress("LongParameterList") -class FileDownloader(private val context: Context) { - - @RequiresPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - suspend fun download( - stringUrl: String, - fileName: String, - filePath: String, - mimeType: String? = null - ): Result { - val url = stringUrl.toURL() ?: return Result.failure(CheckoutException("Malformed URL")) - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - downloadAndSaveApi29AndAbove(context, url, fileName, filePath, mimeType ?: "*/*") - } else { - downloadAndSaveApi28AndBelow(context, url, fileName, filePath) - } - } - - @RequiresPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - private suspend fun downloadAndSaveApi28AndBelow( - context: Context, - url: URL, - fileName: String, - filePath: String, - ): Result = withContext(Dispatchers.IO) { - val imageFile = File(context.getExternalFilesDir(filePath), fileName) - url.openStream().use { input -> - FileOutputStream(imageFile).use { output -> - input.copyTo(output) - - val insertImageResult = MediaStore.Images.Media.insertImage( - context.contentResolver, - imageFile.absolutePath, - fileName, - null - ) - - return@withContext if (insertImageResult == null) { - Result.failure(CheckoutException("couldn't insert image to gallery")) - } else { - Result.success(Unit) - } - } - } - } - - @RequiresApi(Build.VERSION_CODES.Q) - private suspend fun downloadAndSaveApi29AndAbove( - context: Context, - url: URL, - fileName: String, - filePath: String, - mimeType: String - ): Result = withContext(Dispatchers.IO) { - val contentValues = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - put(MediaStore.MediaColumns.MIME_TYPE, mimeType) - put(MediaStore.MediaColumns.RELATIVE_PATH, filePath) - } - val resolver = context.contentResolver - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) - ?: return@withContext Result.failure(CheckoutException("URI is null")) - - url.openStream().use { input -> - resolver.openOutputStream(uri).use { output -> - if (output == null) return@withContext Result.failure(CheckoutException("out is null")) - input.copyTo(output, DEFAULT_BUFFER_SIZE) - return@withContext Result.success(Unit) - } - } - } - - private fun String.toURL(): URL? { - return try { - URL(this) - } catch (e: MalformedURLException) { - Logger.e(TAG, "toURL: $e") - null - } - } - - companion object { - private val TAG = LogUtil.getTag() - } -} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleProvider.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleProvider.kt new file mode 100644 index 0000000000..05b20ad1a9 --- /dev/null +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 9/2/2024. + */ + +package com.adyen.checkout.core.internal.util + +import android.content.Context +import android.os.Build +import androidx.annotation.RestrictTo +import java.util.Locale + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class LocaleProvider { + + fun getLocale(context: Context): Locale { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + context.resources.configuration.locales[0] + } else { + @Suppress("DEPRECATION") + context.resources.configuration.locale + } + } +} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleUtil.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleUtil.kt index c822d8da64..cbd9b3a28f 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleUtil.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LocaleUtil.kt @@ -7,9 +7,8 @@ */ package com.adyen.checkout.core.internal.util -import android.content.Context -import android.os.Build import androidx.annotation.RestrictTo +import com.adyen.checkout.core.exception.CheckoutException import java.util.IllformedLocaleException import java.util.Locale @@ -19,21 +18,6 @@ import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) object LocaleUtil { - /** - * Get the current user Locale. - * @param context The context - * @return The user Locale - */ - @JvmStatic - fun getLocale(context: Context): Locale { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - context.resources.configuration.locales[0] - } else { - @Suppress("DEPRECATION") - context.resources.configuration.locale - } - } - /** * Gets the language tag from a Locale. * @@ -42,6 +26,9 @@ object LocaleUtil { */ @JvmStatic fun toLanguageTag(locale: Locale): String { + if (!isValidLocale(locale)) { + throw CheckoutException("Invalid shopper locale: $locale.") + } return locale.toLanguageTag() } diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LogUtil.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LogUtil.kt deleted file mode 100644 index 3780cdb761..0000000000 --- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LogUtil.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2020 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by caiof on 17/12/2020. - */ -package com.adyen.checkout.core.internal.util - -import android.os.Build -import androidx.annotation.RestrictTo - -/** - * Utility class with methods related to logs. - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -object LogUtil { - - private const val CHECKOUT_LOG_PREFIX = "CO." - private const val CLASS_NOT_FOUND = "?Unknown?" - private const val MAX_TAG_SIZE = 23 - - /** - * Get the TAG to be used for logging inside Checkout classes. - * - * @return A String to be used as TAG. - */ - @JvmStatic - fun getTag(): String = getTag(CHECKOUT_LOG_PREFIX) - - /** - * Get the TAG to be used for logging by the calling class. - * - * @return A String to be used as TAG with the format "Prefix.ClassName" - */ - // This could be used by merchants if they want to. - @JvmStatic - fun getTag(prefix: String): String { - val callerClass = simplifiedCallerClassName - var tag = prefix + callerClass - - // Log tags have a size limitation on API lvl 23 and before - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M && tag.length > MAX_TAG_SIZE) { - tag = tag.substring(0, MAX_TAG_SIZE) - } - return tag - } - - private val simplifiedCallerClassName: String - get() { - val className = callerClassName - return simplifyClassName(className) - } - - private val callerClassName: String - get() { - val stElements = Thread.currentThread().stackTrace - for (ste in stElements.drop(1)) { - val callerClass = ste.className - if (callerClass != LogUtil::class.java.name && callerClass.indexOf("java.lang.Thread") != 0) { - return callerClass - } - } - return CLASS_NOT_FOUND - } - - private fun simplifyClassName(className: String): String { - val packageSplit = className.split(".").toTypedArray() - return if (packageSplit.isEmpty()) className else packageSplit[packageSplit.size - 1] - } -} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LogcatLogger.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LogcatLogger.kt new file mode 100644 index 0000000000..4f72379412 --- /dev/null +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/LogcatLogger.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 1/2/2024. + */ + +package com.adyen.checkout.core.internal.util + +import android.os.Build +import android.util.Log +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.AdyenLogger + +internal class LogcatLogger : AdyenLogger { + + private var minLogLevel: AdyenLogLevel = AdyenLogLevel.NONE + + override fun shouldLog(level: AdyenLogLevel): Boolean { + return level.priority >= minLogLevel.priority + } + + override fun setLogLevel(level: AdyenLogLevel) { + minLogLevel = level + } + + override fun log(level: AdyenLogLevel, tag: String, message: String, throwable: Throwable?) { + // Before API 26 tags have a max length + val trimmedTag = if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + tag + } else { + tag.substring(0, MAX_TAG_LENGTH) + } + + val fullMessage = concatThrowable(message, throwable) + + if (fullMessage.length < MAX_LOG_LENGTH) { + logToLogcat(level.priority, trimmedTag, fullMessage) + return + } + + val divisions = fullMessage.length / MAX_LOG_LENGTH + for (i in 0..divisions) { + val newMessage: String = if (i != divisions) { + fullMessage.substring(i * MAX_LOG_LENGTH, (i + 1) * MAX_LOG_LENGTH) + } else { + fullMessage.substring(i * MAX_LOG_LENGTH) + } + logToLogcat(level.priority, "$trimmedTag-$i", newMessage) + } + } + + private fun concatThrowable(message: String, throwable: Throwable?): String { + return if (throwable != null) { + "$message: ${Log.getStackTraceString(throwable)}" + } else { + message + } + } + + private fun logToLogcat( + priority: Int, + tag: String, + message: String, + ) { + when (priority) { + AdyenLogLevel.NONE.priority -> Unit + + Log.ASSERT -> { + Log.wtf(tag, message) + } + + else -> { + Log.println(priority, tag, message) + } + } + } + + companion object { + private const val MAX_TAG_LENGTH = 23 + private const val MAX_LOG_LENGTH = 2048 + } +} diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/Logger.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/Logger.kt index 2439a1ad31..86a3fa4223 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/Logger.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/Logger.kt @@ -10,171 +10,21 @@ package com.adyen.checkout.core.internal.util import android.util.Log import androidx.annotation.IntDef import androidx.annotation.RestrictTo -import com.adyen.checkout.core.BuildConfig /** * Log manager for Checkout. * Serves as a proxy managing what and how to log information. */ -// Keeping method names to match the ones from Logcat @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -@Suppress("TooManyFunctions") object Logger { @IntDef(SENSITIVE, Log.VERBOSE, Log.DEBUG, Log.INFO, Log.WARN, Log.ERROR, NONE) @Retention(AnnotationRetention.SOURCE) + @Deprecated("Deprecated", ReplaceWith("AdyenLogLevel", "com.adyen.checkout.core.AdyenLogLevel")) annotation class LogLevel - // TODO: 14/02/2019 The idea is for this class to have a system where we can send a stream of logs to the merchant - // and/or proxy to Logcat. private const val SENSITIVE = -1 - const val NONE = Log.ASSERT + 1 - - // The logcat limit changes per device, you can see it using $adb logcat -g - // 2KB seems like a safe value to be within max payload range - private const val MAX_LOGCAT_MSG_SIZE = 2048 - - @LogLevel - private var logLevel = if (BuildConfig.DEBUG) Log.DEBUG else NONE - private var isLogLevelInitialized = false - - fun updateDefaultLogLevel(isDebugBuild: Boolean) { - if (!isLogLevelInitialized) { - logLevel = if (isDebugBuild) Log.DEBUG else NONE - } - } - - internal fun setLogLevel(@LogLevel logLevel: Int) { - isLogLevelInitialized = true - this.logLevel = logLevel - } - - @JvmStatic - fun v(tag: String, msg: String) { - logToLogcat(Log.VERBOSE, tag, msg, null) - } - - @JvmStatic - fun v(tag: String, msg: String, tr: Throwable) { - logToLogcat(Log.VERBOSE, tag, msg, tr) - } - - @JvmStatic - fun d(tag: String, msg: String) { - logToLogcat(Log.DEBUG, tag, msg, null) - } - - @JvmStatic - fun d(tag: String, msg: String, tr: Throwable) { - logToLogcat(Log.DEBUG, tag, msg, tr) - } - - @JvmStatic - fun i(tag: String, msg: String) { - logToLogcat(Log.INFO, tag, msg, null) - } - @JvmStatic - fun i(tag: String, msg: String, tr: Throwable) { - logToLogcat(Log.INFO, tag, msg, tr) - } - - @JvmStatic - fun w(tag: String, msg: String) { - logToLogcat(Log.WARN, tag, msg, null) - } - - @JvmStatic - fun w(tag: String, msg: String, tr: Throwable) { - logToLogcat(Log.WARN, tag, msg, tr) - } - - @JvmStatic - fun e(tag: String, msg: String) { - logToLogcat(Log.ERROR, tag, msg, null) - } - - @JvmStatic - fun e(tag: String, msg: String, tr: Throwable) { - logToLogcat(Log.ERROR, tag, msg, tr) - } - - /** - * Log to be used when you want to debug sensitive information that cannot be committed. - * Set the [LogLevel] to [SENSITIVE] and make sure to change it back before committing. - * - * @param tag Used to identify the source of a log message. - * @param msg The message you would like logged. - */ - @Suppress("unused") - fun sensitiveLog(tag: String, msg: String) { - if (logLevel != SENSITIVE) { - throw SecurityException("Sensitive information should never be logged. Remove before committing.") - } else { - logToLogcat(SENSITIVE, tag, msg, null) - } - } - - @Suppress("CyclomaticComplexMethod") - private fun logToLogcat(@LogLevel logLevel: Int, tag: String, msg: String, tr: Throwable?) { - if (this.logLevel > logLevel) { - return - } - - // Cut the message into multiple logs if it's too big - if (msg.length > MAX_LOGCAT_MSG_SIZE) { - logInChunks(logLevel, tag, msg, tr) - return - } - - when (logLevel) { - SENSITIVE -> if (tr == null) { - Log.wtf(tag, msg) - } else { - Log.wtf(tag, msg, tr) - } - Log.VERBOSE -> if (tr == null) { - Log.v(tag, msg) - } else { - Log.v(tag, msg, tr) - } - Log.DEBUG -> if (tr == null) { - Log.d(tag, msg) - } else { - Log.d(tag, msg, tr) - } - Log.INFO -> if (tr == null) { - Log.i(tag, msg) - } else { - Log.i(tag, msg, tr) - } - Log.WARN -> if (tr == null) { - Log.w(tag, msg) - } else { - Log.w(tag, msg, tr) - } - Log.ERROR -> if (tr == null) { - Log.e(tag, msg) - } else { - Log.e(tag, msg, tr) - } - NONE -> {} - else -> {} - } - } - - private fun logInChunks(@LogLevel logLevel: Int, tag: String, msg: String, tr: Throwable?) { - val divisions = msg.length / MAX_LOGCAT_MSG_SIZE - for (i in 0..divisions) { - val newMessage: String = if (i != divisions) { - msg.substring( - i * MAX_LOGCAT_MSG_SIZE, - (i + 1) * MAX_LOGCAT_MSG_SIZE - ) - } else { - msg.substring(i * MAX_LOGCAT_MSG_SIZE) - } - logToLogcat(logLevel, "$tag-$i", newMessage, tr) - } - } + @Deprecated("Deprecated", ReplaceWith("AdyenLogLevel.NONE", "com.adyen.checkout.core.AdyenLogLevel")) + const val NONE = Log.ASSERT + 1 } diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/RunCompileOnly.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/RunCompileOnly.kt index e378f1eb45..22d1852b99 100644 --- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/RunCompileOnly.kt +++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/RunCompileOnly.kt @@ -9,15 +9,16 @@ package com.adyen.checkout.core.internal.util import androidx.annotation.RestrictTo +import com.adyen.checkout.core.AdyenLogLevel @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) inline fun runCompileOnly(block: () -> R): R? { try { return block() } catch (e: ClassNotFoundException) { - Logger.w(LogUtil.getTag(), "Class not found. Are you missing a dependency?", e) + adyenLog(AdyenLogLevel.WARN, "runCompileOnly", e) { "Class not found. Are you missing a dependency?" } } catch (e: NoClassDefFoundError) { - Logger.w(LogUtil.getTag(), "Class not found. Are you missing a dependency?", e) + adyenLog(AdyenLogLevel.WARN, "runCompileOnly", e) { "Class not found. Are you missing a dependency?" } } return null diff --git a/checkout-core/src/test/java/com/adyen/checkout/core/internal/util/AdyenLoggerTest.kt b/checkout-core/src/test/java/com/adyen/checkout/core/internal/util/AdyenLoggerTest.kt new file mode 100644 index 0000000000..d84f6005be --- /dev/null +++ b/checkout-core/src/test/java/com/adyen/checkout/core/internal/util/AdyenLoggerTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 5/2/2024. + */ + +package com.adyen.checkout.core.internal.util + +import android.util.Log +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.AdyenLogger +import com.adyen.checkout.core.exception.CheckoutException +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource + +internal class AdyenLoggerTest { + + private lateinit var logger: TestLogger + + @BeforeEach + fun setup() { + logger = TestLogger() + } + + @Test + fun `when logger is set, then logger instance is of correct type`() { + AdyenLogger.setLogger(logger) + + assertEquals(logger, AdyenLogger.logger) + } + + @Test + fun `when logger is set, then logging is correctly propagated`() { + AdyenLogger.setLogger(logger) + AdyenLogger.setLogLevel(AdyenLogLevel.VERBOSE) + val exception = CheckoutException("Test") + + adyenLog(AdyenLogLevel.INFO, exception) { "test" } + + val expected = LogData(AdyenLogLevel.INFO, "CO.AdyenLoggerTest", "test", exception) + logger.assertLogCalled(expected) + } + + @Test + fun `when logger is reset, then logger is back to default type`() { + AdyenLogger.setLogger(logger) + + AdyenLogger.resetLogger() + + assertInstanceOf(LogcatLogger::class.java, AdyenLogger.logger) + } + + @Test + fun `when log level is set, then it is correctly propagated`() { + AdyenLogger.setLogger(logger) + + AdyenLogger.setLogLevel(AdyenLogLevel.ASSERT) + + logger.assertLogLevel(AdyenLogLevel.ASSERT) + } + + @Test + fun `when log level is set, then log is not executed`() { + AdyenLogger.setLogger(logger) + AdyenLogger.setLogLevel(AdyenLogLevel.ERROR) + + adyenLog(AdyenLogLevel.VERBOSE) { "test" } + + logger.assertLogNotCalled() + } + + @Suppress("DEPRECATION") + @ParameterizedTest + @MethodSource("logLevelSource") + fun `when old LogLevel is, then it is mapped to AdyenLogLevel`( + @Logger.LogLevel oldLogLevel: Int, + adyenLogLevel: AdyenLogLevel + ) { + AdyenLogger.setLogger(logger) + + AdyenLogger.setLogLevel(oldLogLevel) + + logger.assertLogLevel(adyenLogLevel) + } + + private class TestLogger : AdyenLogger { + + private var minLogLevel: AdyenLogLevel = AdyenLogLevel.NONE + + private var lastLog: LogData? = null + + override fun shouldLog(level: AdyenLogLevel): Boolean { + return level.priority >= minLogLevel.priority + } + + override fun setLogLevel(level: AdyenLogLevel) { + this.minLogLevel = level + } + + override fun log(level: AdyenLogLevel, tag: String, message: String, throwable: Throwable?) { + lastLog = LogData(level, tag, message, throwable) + } + + fun assertLogLevel(expected: AdyenLogLevel) { + assertEquals(expected, minLogLevel) + } + + fun assertLogCalled(expected: LogData) { + assertEquals(expected, lastLog) + } + + fun assertLogNotCalled() { + assertNull(lastLog) + } + } + + private data class LogData( + val level: AdyenLogLevel, + val tag: String, + val message: String, + val throwable: Throwable?, + ) + + companion object { + + @Suppress("DEPRECATION") + @JvmStatic + fun logLevelSource() = listOf( + arguments(Log.VERBOSE, AdyenLogLevel.VERBOSE), + arguments(Log.DEBUG, AdyenLogLevel.DEBUG), + arguments(Log.INFO, AdyenLogLevel.INFO), + arguments(Log.WARN, AdyenLogLevel.WARN), + arguments(Log.ERROR, AdyenLogLevel.ERROR), + arguments(Log.ASSERT, AdyenLogLevel.NONE), + arguments(Logger.NONE, AdyenLogLevel.NONE), + ) + } +} diff --git a/components-compose/src/main/java/com/adyen/checkout/components/compose/ComposeExtensions.kt b/components-compose/src/main/java/com/adyen/checkout/components/compose/ComposeExtensions.kt index ce95a86bf8..e410b177f1 100644 --- a/components-compose/src/main/java/com/adyen/checkout/components/compose/ComposeExtensions.kt +++ b/components-compose/src/main/java/com/adyen/checkout/components/compose/ComposeExtensions.kt @@ -6,6 +6,8 @@ * Created by josephj on 17/5/2023. */ +@file:Suppress("TooManyFunctions") + package com.adyen.checkout.components.compose import android.app.Application @@ -16,6 +18,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSavedStateRegistryOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentComponentState @@ -34,6 +37,250 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionStoredPaymentCo import com.adyen.checkout.ui.core.AdyenComponentView import com.adyen.checkout.ui.core.internal.ui.ViewableComponent +//region CheckoutConfiguration + +/** + * Get a [PaymentComponent] from a [Composable]. + * + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ +@Composable +fun < + ComponentT : PaymentComponent, + ConfigurationT : Configuration, + ComponentStateT : PaymentComponentState<*>, + ComponentCallbackT : ComponentCallback + > PaymentComponentProvider.get( + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String?, + order: Order? = null, +): ComponentT { + return get( + savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, + viewModelStoreOwner = LocalViewModelStoreOwner.current + ?: throw ComponentException("Cannot find current LocalViewModelStoreOwner"), + lifecycleOwner = LocalLifecycleOwner.current, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = LocalContext.current.applicationContext as Application, + componentCallback = componentCallback, + order = order, + key = key, + ) +} + +/** + * Get a [PaymentComponent] with a stored payment method from a [Composable]. + * + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ +@Composable +fun < + ComponentT : PaymentComponent, + ConfigurationT : Configuration, + ComponentStateT : PaymentComponentState<*>, + ComponentCallbackT : ComponentCallback + > StoredPaymentComponentProvider.get( + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String?, + order: Order? = null, +): ComponentT { + return get( + savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, + viewModelStoreOwner = LocalViewModelStoreOwner.current + ?: throw ComponentException("Cannot find current LocalViewModelStoreOwner"), + lifecycleOwner = LocalLifecycleOwner.current, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = LocalContext.current.applicationContext as Application, + componentCallback = componentCallback, + order = order, + key = key, + ) +} + +/** + * Get a [PaymentComponent] with a checkout session from a [Composable]. You only need to integrate with the /sessions + * endpoint to create a session and the component will automatically handle the rest of the payment flow. + * + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ +@Composable +fun < + ComponentT : PaymentComponent, + ConfigurationT : Configuration, + ComponentStateT : PaymentComponentState<*>, + ComponentCallbackT : SessionComponentCallback + > SessionPaymentComponentProvider.get( + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String, +): ComponentT { + return get( + savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, + viewModelStoreOwner = LocalViewModelStoreOwner.current + ?: throw ComponentException("Cannot find current LocalViewModelStoreOwner"), + lifecycleOwner = LocalLifecycleOwner.current, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = LocalContext.current.applicationContext as Application, + componentCallback = componentCallback, + key = key, + ) +} + +/** + * Get a [PaymentComponent] with a checkout session from a [Composable]. You only need to integrate with the /sessions + * endpoint to create a session and the component will automatically handle the rest of the payment flow. + * + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ +@Composable +fun < + ComponentT : PaymentComponent, + ConfigurationT : Configuration, + ComponentStateT : PaymentComponentState<*>, + ComponentCallbackT : SessionComponentCallback + > SessionPaymentComponentProvider.get( + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + componentCallback: ComponentCallbackT, + key: String, +): ComponentT { + return get( + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutSession.getConfiguration(), + componentCallback = componentCallback, + key = key, + ) +} + +/** + * Get a [PaymentComponent] with a stored payment method and a checkout session from a [Composable]. You only need to + * integrate with the /sessions endpoint to create a session and the component will automatically handle the rest of + * the payment flow. + * + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ +@Composable +fun < + ComponentT : PaymentComponent, + ConfigurationT : Configuration, + ComponentStateT : PaymentComponentState<*>, + ComponentCallbackT : SessionComponentCallback + > SessionStoredPaymentComponentProvider.get( + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String?, +): ComponentT { + return get( + savedStateRegistryOwner = LocalSavedStateRegistryOwner.current, + viewModelStoreOwner = LocalViewModelStoreOwner.current + ?: throw ComponentException("Cannot find current LocalViewModelStoreOwner"), + lifecycleOwner = LocalLifecycleOwner.current, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = LocalContext.current.applicationContext as Application, + componentCallback = componentCallback, + key = key, + ) +} + +/** + * Get a [PaymentComponent] with a stored payment method and a checkout session from a [Composable]. You only need to + * integrate with the /sessions endpoint to create a session and the component will automatically handle the rest of + * the payment flow. + * + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ +@Composable +fun < + ComponentT : PaymentComponent, + ConfigurationT : Configuration, + ComponentStateT : PaymentComponentState<*>, + ComponentCallbackT : SessionComponentCallback + > SessionStoredPaymentComponentProvider.get( + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + componentCallback: ComponentCallbackT, + key: String?, +): ComponentT { + return get( + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutSession.getConfiguration(), + componentCallback = componentCallback, + key = key, + ) +} + +//endregion + +//region Generic configuration + /** * Get a [PaymentComponent] from a [Composable]. * @@ -201,6 +448,8 @@ fun < ) } +//endregion + /** * A [Composable] that can display input and fill in details for a [Component]. */ diff --git a/components-core/build.gradle b/components-core/build.gradle index cc9f4fdd47..64822b57d0 100644 --- a/components-core/build.gradle +++ b/components-core/build.gradle @@ -45,6 +45,7 @@ dependencies { //Tests testImplementation project(':test-core') + testImplementation testLibraries.json testImplementation testLibraries.junit5 testImplementation testLibraries.androidx.lifecycle testImplementation testLibraries.kotlinCoroutines diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/ActionComponentCallback.kt b/components-core/src/main/java/com/adyen/checkout/components/core/ActionComponentCallback.kt index c102d21753..e978c581fe 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/ActionComponentCallback.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/ActionComponentCallback.kt @@ -9,6 +9,7 @@ package com.adyen.checkout.components.core import com.adyen.checkout.components.core.internal.ActionComponent +import com.adyen.checkout.core.PermissionHandlerCallback import org.json.JSONObject /** @@ -38,4 +39,18 @@ interface ActionComponentCallback { * @param componentError The error encountered. */ fun onError(componentError: ComponentError) + + /** + * Should be overridden to support runtime permissions for components. + * Runtime permission should be requested and communicated back through the callback. + * If not overridden, [PermissionHandlerCallback.onPermissionRequestNotHandled] will be invoked, which will show an + * error message. + * + * @param requiredPermission Required runtime permission. + * @param permissionCallback Callback to be used when passing permission result. + */ + fun onPermissionRequest(requiredPermission: String, permissionCallback: PermissionHandlerCallback) { + // To be optionally overridden + permissionCallback.onPermissionRequestNotHandled(requiredPermission) + } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/AddressData.kt b/components-core/src/main/java/com/adyen/checkout/components/core/AddressData.kt new file mode 100644 index 0000000000..c35fda6420 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/AddressData.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 6/2/2024. + */ + +package com.adyen.checkout.components.core + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel + +data class AddressData( + val postalCode: String, + val street: String, + val stateOrProvince: String, + val houseNumberOrName: String, + val apartmentSuite: String?, + val city: String, + val country: String, +) + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun AddressData.mapToAddressInputModel() = AddressInputModel( + postalCode = postalCode, + street = street, + stateOrProvince = stateOrProvince, + houseNumberOrName = houseNumberOrName, + apartmentSuite = apartmentSuite.orEmpty(), + city = city, + country = country, +) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/AddressLookupCallback.kt b/components-core/src/main/java/com/adyen/checkout/components/core/AddressLookupCallback.kt new file mode 100644 index 0000000000..43649e171e --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/AddressLookupCallback.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 11/12/2023. + */ + +package com.adyen.checkout.components.core + +/** + * Implement this callback to be able to use Address Lookup functionality. + */ +interface AddressLookupCallback { + + /** + * In this method you will receive the query as shopper types it. + * + * This query is to be used to perform an address search operation. + * + * @param query The search query. + */ + fun onQueryChanged(query: String) + + /** + * In this method you will receive a [LookupAddress] object that is incomplete. + * + * This callback should be used to retrieve the complete details of the given [LookupAddress]. + * + * @param lookupAddress The address. + */ + fun onLookupCompletion(lookupAddress: LookupAddress) = false +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/AddressLookupResult.kt b/components-core/src/main/java/com/adyen/checkout/components/core/AddressLookupResult.kt new file mode 100644 index 0000000000..e9eddc0cd0 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/AddressLookupResult.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 2/1/2024. + */ + +package com.adyen.checkout.components.core + +/** + * A class that contains the result of address lookup completion call. + */ +sealed class AddressLookupResult { + /** + * An error occurred while making of the call. + * + * @param message Error message to be shown to shopper. + */ + data class Error(val message: String? = null) : AddressLookupResult() + + /** + * Completion call has been successfully completed. + * + * @param lookupAddress The complete address details. + */ + data class Completed(val lookupAddress: LookupAddress) : AddressLookupResult() +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/CheckoutConfiguration.kt b/components-core/src/main/java/com/adyen/checkout/components/core/CheckoutConfiguration.kt new file mode 100644 index 0000000000..483351e670 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/CheckoutConfiguration.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 5/12/2023. + */ + +package com.adyen.checkout.components.core + +import android.annotation.SuppressLint +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.CONTENTS_FILE_DESCRIPTOR +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker +import com.adyen.checkout.core.Environment +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.internal.util.LocaleUtil +import kotlinx.parcelize.IgnoredOnParcel +import java.util.Locale + +/** + * A generic configuration class that allows customizing the Checkout library. + * You can use the block parameter to add drop-in or payment method specific configurations. For example: + * + * ``` + * val checkoutConfiguration = CheckoutConfiguration( + * environment, + * clientKey, + * shopperLocale, // optional + * amount, // not applicable with the Sessions flow + * ) { + * dropIn { + * setEnableRemovingStoredPaymentMethods(true) + * } + * card { + * setHolderNameRequired(true) + * } + * } + * ``` + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + * @param shopperLocale The [Locale] used to display information to the shopper. By default the shopper locale will + * match the value passed to the API with the sessions flow, or the primary user locale on the device otherwise. Check + * out the [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * @param amount The amount of the transaction. Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set this + * value. + * @param analyticsConfiguration A configuration for the internal analytics of the library. + * @param configurationBlock A block that allows adding drop-in or payment method specific configurations. + */ +@CheckoutConfigurationMarker +class CheckoutConfiguration( + override val environment: Environment, + override val clientKey: String, + override val shopperLocale: Locale? = null, + override val amount: Amount? = null, + override val analyticsConfiguration: AnalyticsConfiguration? = null, + @IgnoredOnParcel + private val configurationBlock: CheckoutConfiguration.() -> Unit = {}, +) : Configuration { + + private val availableConfigurations = mutableMapOf() + + init { + apply(configurationBlock) + validateContents() + } + + private fun validateContents() { + shopperLocale?.let { + if (!LocaleUtil.isValidLocale(it)) { + throw CheckoutException("Invalid shopper locale: $shopperLocale.") + } + } + } + + // We need custom parcelization for this class to parcelize availableConfigurations. + @SuppressLint("ParcelClassLoader") + @Suppress("UNCHECKED_CAST", "DEPRECATION") + private constructor(parcel: Parcel) : this( + // the order in which these fields are read from Parcel should match the order in which they are written to + // Parcel in the `writeToParcel` function + shopperLocale = parcel.readSerializable() as? Locale, + environment = requireNotNull(parcel.readParcelable(Environment::class.java.classLoader)), + clientKey = requireNotNull(parcel.readString()), + amount = parcel.readParcelable(Amount::class.java.classLoader), + analyticsConfiguration = parcel.readParcelable(Amount::class.java.classLoader), + ) { + val size = parcel.readInt() + + repeat(size) { + val key = requireNotNull(parcel.readString()) + val configClass = parcel.readSerializable() as Class + val config = requireNotNull(parcel.readParcelable(configClass.classLoader)) + availableConfigurations[key] = config + } + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun addConfiguration(key: String, configuration: Configuration) { + availableConfigurations[key] = configuration + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun addActionConfiguration(configuration: Configuration) { + availableConfigurations[configuration::class.java.simpleName] = configuration + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getConfiguration(key: String): T? { + @Suppress("UNCHECKED_CAST") + return availableConfigurations[key] as? T + } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getActionConfiguration(configClass: Class): T? { + @Suppress("UNCHECKED_CAST") + return availableConfigurations[configClass.simpleName] as? T + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + // the order in which these fields are written from Parcel should match the order in which they are read from + // Parcel in `constructor(parcel: Parcel)` + dest.writeSerializable(shopperLocale) + dest.writeParcelable(environment, flags) + dest.writeString(clientKey) + dest.writeParcelable(amount, flags) + dest.writeParcelable(analyticsConfiguration, flags) + dest.writeInt(availableConfigurations.size) + availableConfigurations.forEach { + dest.writeString(it.key) + dest.writeSerializable(it.value::class.java) + dest.writeParcelable(it.value, flags) + } + } + + override fun describeContents(): Int = CONTENTS_FILE_DESCRIPTOR + + companion object CREATOR : Parcelable.Creator { + + override fun createFromParcel(source: Parcel): CheckoutConfiguration { + return CheckoutConfiguration(source) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/ComponentCallback.kt b/components-core/src/main/java/com/adyen/checkout/components/core/ComponentCallback.kt index dc6d68431d..1bc6cb4371 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/ComponentCallback.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/ComponentCallback.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.components.core import com.adyen.checkout.components.core.internal.BaseComponentCallback import com.adyen.checkout.components.core.internal.PaymentComponent +import com.adyen.checkout.core.PermissionHandlerCallback import org.json.JSONObject /** @@ -71,4 +72,18 @@ interface ComponentCallback> : BaseComponentCallbac * @param state The state of the payment component at the current moment. */ fun onStateChanged(state: T) = Unit + + /** + * Should be overridden to support runtime permissions for components. + * Runtime permission should be requested and communicated back through the callback. + * If not overridden, [PermissionHandlerCallback.onPermissionRequestNotHandled] will be invoked, which will show an + * error message. + * + * @param requiredPermission Required runtime permission. + * @param permissionCallback Callback to be used when passing permission result. + */ + fun onPermissionRequest(requiredPermission: String, permissionCallback: PermissionHandlerCallback) { + // To be optionally overridden + permissionCallback.onPermissionRequestNotHandled(requiredPermission) + } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/LookupAddress.kt b/components-core/src/main/java/com/adyen/checkout/components/core/LookupAddress.kt new file mode 100644 index 0000000000..d4ad0ae8f2 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/LookupAddress.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.components.core + +data class LookupAddress( + val id: String, + val address: AddressData +) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentComponentData.kt b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentComponentData.kt index 919ee82ffc..aeb4b7a57a 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentComponentData.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentComponentData.kt @@ -37,6 +37,7 @@ data class PaymentComponentData( var dateOfBirth: String? = null, var socialSecurityNumber: String? = null, var installments: Installments? = null, + var supportNativeRedirect: Boolean? = true, ) : ModelObject() { companion object { @@ -53,6 +54,7 @@ data class PaymentComponentData( private const val SOCIAL_SECURITY_NUMBER = "socialSecurityNumber" private const val INSTALLMENTS = "installments" private const val ORDER = "order" + private const val SUPPORT_NATIVE_REDIRECT = "supportNativeRedirect" @JvmField val SERIALIZER: Serializer> = object : Serializer> { @@ -60,9 +62,10 @@ data class PaymentComponentData( return try { JSONObject().apply { putOpt(PAYMENT_METHOD, serializeOpt(modelObject.paymentMethod, PaymentMethodDetails.SERIALIZER)) + putOpt(ORDER, serializeOpt(modelObject.order, OrderRequest.SERIALIZER)) + putOpt(AMOUNT, serializeOpt(modelObject.amount, Amount.SERIALIZER)) putOpt(STORE_PAYMENT_METHOD, modelObject.storePaymentMethod) putOpt(SHOPPER_REFERENCE, modelObject.shopperReference) - putOpt(AMOUNT, serializeOpt(modelObject.amount, Amount.SERIALIZER)) putOpt(BILLING_ADDRESS, serializeOpt(modelObject.billingAddress, Address.SERIALIZER)) putOpt(DELIVERY_ADDRESS, serializeOpt(modelObject.deliveryAddress, Address.SERIALIZER)) putOpt(SHOPPER_NAME, serializeOpt(modelObject.shopperName, ShopperName.SERIALIZER)) @@ -71,7 +74,7 @@ data class PaymentComponentData( putOpt(DATE_OF_BIRTH, modelObject.dateOfBirth) putOpt(SOCIAL_SECURITY_NUMBER, modelObject.socialSecurityNumber) putOpt(INSTALLMENTS, serializeOpt(modelObject.installments, Installments.SERIALIZER)) - putOpt(ORDER, serializeOpt(modelObject.order, OrderRequest.SERIALIZER)) + putOpt(SUPPORT_NATIVE_REDIRECT, modelObject.supportNativeRedirect) } } catch (e: JSONException) { throw ModelSerializationException(PaymentComponentData::class.java, e) @@ -84,9 +87,10 @@ data class PaymentComponentData( jsonObject.optJSONObject(PAYMENT_METHOD), PaymentMethodDetails.SERIALIZER ), + order = deserializeOpt(jsonObject.optJSONObject(ORDER), OrderRequest.SERIALIZER), + amount = deserializeOpt(jsonObject.optJSONObject(AMOUNT), Amount.SERIALIZER), storePaymentMethod = jsonObject.optBoolean(STORE_PAYMENT_METHOD), shopperReference = jsonObject.optString(SHOPPER_REFERENCE), - amount = deserializeOpt(jsonObject.optJSONObject(AMOUNT), Amount.SERIALIZER), billingAddress = deserializeOpt(jsonObject.optJSONObject(BILLING_ADDRESS), Address.SERIALIZER), deliveryAddress = deserializeOpt(jsonObject.optJSONObject(DELIVERY_ADDRESS), Address.SERIALIZER), shopperName = deserializeOpt(jsonObject.optJSONObject(SHOPPER_NAME), ShopperName.SERIALIZER), @@ -95,7 +99,7 @@ data class PaymentComponentData( dateOfBirth = jsonObject.optString(DATE_OF_BIRTH), socialSecurityNumber = jsonObject.optString(SOCIAL_SECURITY_NUMBER), installments = deserializeOpt(jsonObject.optJSONObject(INSTALLMENTS), Installments.SERIALIZER), - order = deserializeOpt(jsonObject.optJSONObject(ORDER), OrderRequest.SERIALIZER), + supportNativeRedirect = jsonObject.optBoolean(SUPPORT_NATIVE_REDIRECT), ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt index db25896506..d8c8e4b29b 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/PaymentMethodTypes.kt @@ -56,6 +56,7 @@ object PaymentMethodTypes { const val PIX = "pix" const val PROMPT_PAY = "promptpay" const val WECHAT_PAY_SDK = "wechatpaySDK" + const val MULTIBANCO = "multibanco" // Voucher payment methods that are not yet supported const val DOKU = "doku" @@ -79,7 +80,6 @@ object PaymentMethodTypes { const val ECONTEXT_ONLINE = "econtext_online" const val ECONTEXT_SEVEN_ELEVEN = "econtext_seven_eleven" const val ECONTEXT_STORES = "econtext_stores" - const val MULTIBANCO = "multibanco" const val OXXO = "oxxo" // Payment methods that might be interpreted as redirect, but are actually not supported @@ -105,6 +105,10 @@ object PaymentMethodTypes { CASH_APP_PAY, DOTPAY, DUIT_NOW, + ECONTEXT_ATM, + ECONTEXT_ONLINE, + ECONTEXT_SEVEN_ELEVEN, + ECONTEXT_STORES, ENTERCASH, EPS, GIFTCARD, @@ -117,6 +121,7 @@ object PaymentMethodTypes { MOLPAY_VIETNAM, ONLINE_BANKING_CZ, ONLINE_BANKING_PL, + ONLINE_BANKING_SK, OPEN_BANKING, PAY_BY_BANK, PAY_NOW, @@ -137,6 +142,7 @@ object PaymentMethodTypes { PIX, PROMPT_PAY, WECHAT_PAY_SDK, + MULTIBANCO, ) // Payment methods that are explicitly unsupported @@ -160,11 +166,6 @@ object PaymentMethodTypes { DRAGONPAY_OTC_BANKING, DRAGONPAY_OTC_NON_BANKING, DRAGONPAY_OTC_PHILIPPINES, - ECONTEXT_ATM, - ECONTEXT_ONLINE, - ECONTEXT_SEVEN_ELEVEN, - ECONTEXT_STORES, - MULTIBANCO, OXXO, WECHAT_PAY_MINI_PROGRAM, WECHAT_PAY_QR, diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/Action.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/Action.kt index cc3690ecce..05e025517a 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/Action.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/Action.kt @@ -52,7 +52,9 @@ abstract class Action : ModelObject() { fun getChildSerializer(actionType: String): Serializer { val childSerializer = when (actionType) { - RedirectAction.ACTION_TYPE -> RedirectAction.SERIALIZER + RedirectAction.ACTION_TYPE, + ActionTypes.NATIVE_REDIRECT -> RedirectAction.SERIALIZER + Threeds2FingerprintAction.ACTION_TYPE -> Threeds2FingerprintAction.SERIALIZER Threeds2ChallengeAction.ACTION_TYPE -> Threeds2ChallengeAction.SERIALIZER Threeds2Action.ACTION_TYPE -> Threeds2Action.SERIALIZER diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionTypes.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/ActionTypes.kt similarity index 83% rename from components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionTypes.kt rename to components-core/src/main/java/com/adyen/checkout/components/core/action/ActionTypes.kt index 0d5139095f..2361a59a0c 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionTypes.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/ActionTypes.kt @@ -5,15 +5,16 @@ * * Created by caiof on 24/8/2020. */ -package com.adyen.checkout.components.core.internal +package com.adyen.checkout.components.core.action /** * Helper class with a list of all the currently supported Actions on Components and Drop-In. */ -internal object ActionTypes { +object ActionTypes { const val AWAIT = "await" const val QR_CODE = "qrCode" const val REDIRECT = "redirect" + const val NATIVE_REDIRECT = "nativeRedirect" const val SDK = "sdk" const val THREEDS2_CHALLENGE = "threeDS2Challenge" const val THREEDS2_FINGERPRINT = "threeDS2Fingerprint" diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/AwaitAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/AwaitAction.kt index 4d9e15622f..fdd0ea4bb3 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/AwaitAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/AwaitAction.kt @@ -7,7 +7,6 @@ */ package com.adyen.checkout.components.core.action -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.getStringOrNull import kotlinx.parcelize.Parcelize diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/QrCodeAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/QrCodeAction.kt index 3f64c573b3..a2ef040878 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/QrCodeAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/QrCodeAction.kt @@ -7,7 +7,6 @@ */ package com.adyen.checkout.components.core.action -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.getStringOrNull import kotlinx.parcelize.Parcelize diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/RedirectAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/RedirectAction.kt index 3bc3275bd4..48d3f29d07 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/RedirectAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/RedirectAction.kt @@ -7,7 +7,6 @@ */ package com.adyen.checkout.components.core.action -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.getStringOrNull import kotlinx.parcelize.Parcelize @@ -21,12 +20,14 @@ data class RedirectAction( override var paymentMethodType: String? = null, var method: String? = null, var url: String? = null, + var nativeRedirectData: String? = null, ) : Action() { companion object { const val ACTION_TYPE = ActionTypes.REDIRECT private const val METHOD = "method" private const val URL = "url" + private const val NATIVE_REDIRECT_DATA = "nativeRedirectData" @JvmField val SERIALIZER: Serializer = object : Serializer { @@ -38,6 +39,7 @@ data class RedirectAction( putOpt(PAYMENT_METHOD_TYPE, modelObject.paymentMethodType) putOpt(METHOD, modelObject.method) putOpt(URL, modelObject.url) + putOpt(NATIVE_REDIRECT_DATA, modelObject.nativeRedirectData) } } catch (e: JSONException) { throw ModelSerializationException(RedirectAction::class.java, e) @@ -51,6 +53,7 @@ data class RedirectAction( paymentMethodType = jsonObject.getStringOrNull(PAYMENT_METHOD_TYPE), method = jsonObject.getStringOrNull(METHOD), url = jsonObject.getStringOrNull(URL), + nativeRedirectData = jsonObject.getStringOrNull(NATIVE_REDIRECT_DATA), ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt index e0d01fd600..68b9bd4186 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/SdkAction.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.components.core.action import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.ModelUtils.deserializeOpt diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2Action.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2Action.kt index da0b055348..4874f0e431 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2Action.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2Action.kt @@ -7,7 +7,6 @@ */ package com.adyen.checkout.components.core.action -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.getStringOrNull import kotlinx.parcelize.Parcelize diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2ChallengeAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2ChallengeAction.kt index 06c420a6be..5531a458c4 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2ChallengeAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2ChallengeAction.kt @@ -7,7 +7,6 @@ */ package com.adyen.checkout.components.core.action -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.getStringOrNull import kotlinx.parcelize.Parcelize diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2FingerprintAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2FingerprintAction.kt index 6a93c287f3..7ad8d2320d 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2FingerprintAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/Threeds2FingerprintAction.kt @@ -7,7 +7,6 @@ */ package com.adyen.checkout.components.core.action -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.getStringOrNull import kotlinx.parcelize.Parcelize diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/action/VoucherAction.kt b/components-core/src/main/java/com/adyen/checkout/components/core/action/VoucherAction.kt index ae7dc811d9..36134e38e9 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/action/VoucherAction.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/action/VoucherAction.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.components.core.action import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ActionTypes import com.adyen.checkout.core.exception.ModelSerializationException import com.adyen.checkout.core.internal.data.model.ModelUtils.deserializeOpt import com.adyen.checkout.core.internal.data.model.ModelUtils.serializeOpt @@ -22,31 +21,41 @@ data class VoucherAction( override var type: String? = null, override var paymentData: String? = null, override var paymentMethodType: String? = null, + var entity: String? = null, var surcharge: Amount? = null, var initialAmount: Amount? = null, var totalAmount: Amount? = null, var issuer: String? = null, var expiresAt: String? = null, var reference: String? = null, + var collectionInstitutionNumber: String? = null, + var maskedTelephoneNumber: String? = null, var alternativeReference: String? = null, var merchantName: String? = null, + var merchantReference: String? = null, // TODO: remove url when it's fixed from backend side var url: String? = null, - var downloadUrl: String? = null + var downloadUrl: String? = null, + var instructionsUrl: String? = null, ) : Action() { companion object { const val ACTION_TYPE = ActionTypes.VOUCHER + private const val ENTITY = "entity" private const val SURCHARGE = "surcharge" private const val INITIAL_AMOUNT = "initialAmount" private const val TOTAL_AMOUNT = "totalAmount" private const val ISSUER = "issuer" private const val EXPIRES_AT = "expiresAt" private const val REFERENCE = "reference" + private const val COLLECTION_INSTITUTION_NUMBER = "collectionInstitutionNumber" + private const val MASKED_TELEPHONE_NUMBER = "maskedTelephoneNumber" private const val ALTERNATIVE_REFERENCE = "alternativeReference" private const val MERCHANT_NAME = "merchantName" + private const val MERCHANT_REFERENCE = "merchantReference" private const val URL = "url" private const val DOWNLOAD_URL = "downloadUrl" + private const val INSTRUCTIONS_URL = "instructionsUrl" @JvmField val SERIALIZER: Serializer = object : Serializer { @@ -56,16 +65,21 @@ data class VoucherAction( putOpt(TYPE, modelObject.type) putOpt(PAYMENT_DATA, modelObject.paymentData) putOpt(PAYMENT_METHOD_TYPE, modelObject.paymentMethodType) + putOpt(ENTITY, modelObject.entity) putOpt(SURCHARGE, serializeOpt(modelObject.surcharge, Amount.SERIALIZER)) putOpt(INITIAL_AMOUNT, serializeOpt(modelObject.initialAmount, Amount.SERIALIZER)) putOpt(TOTAL_AMOUNT, serializeOpt(modelObject.totalAmount, Amount.SERIALIZER)) putOpt(ISSUER, modelObject.issuer) putOpt(EXPIRES_AT, modelObject.expiresAt) putOpt(REFERENCE, modelObject.reference) + putOpt(COLLECTION_INSTITUTION_NUMBER, modelObject.collectionInstitutionNumber) + putOpt(MASKED_TELEPHONE_NUMBER, modelObject.maskedTelephoneNumber) putOpt(ALTERNATIVE_REFERENCE, modelObject.alternativeReference) putOpt(MERCHANT_NAME, modelObject.merchantName) + putOpt(MERCHANT_REFERENCE, modelObject.merchantReference) putOpt(URL, modelObject.url) putOpt(DOWNLOAD_URL, modelObject.downloadUrl) + putOpt(INSTRUCTIONS_URL, modelObject.instructionsUrl) } } catch (e: JSONException) { throw ModelSerializationException(VoucherAction::class.java, e) @@ -77,16 +91,21 @@ data class VoucherAction( type = jsonObject.getStringOrNull(TYPE), paymentData = jsonObject.getStringOrNull(PAYMENT_DATA), paymentMethodType = jsonObject.getStringOrNull(PAYMENT_METHOD_TYPE), + entity = jsonObject.getStringOrNull(ENTITY), surcharge = deserializeOpt(jsonObject.optJSONObject(SURCHARGE), Amount.SERIALIZER), initialAmount = deserializeOpt(jsonObject.optJSONObject(INITIAL_AMOUNT), Amount.SERIALIZER), totalAmount = deserializeOpt(jsonObject.optJSONObject(TOTAL_AMOUNT), Amount.SERIALIZER), issuer = jsonObject.getStringOrNull(ISSUER), expiresAt = jsonObject.getStringOrNull(EXPIRES_AT), reference = jsonObject.getStringOrNull(REFERENCE), + collectionInstitutionNumber = jsonObject.getStringOrNull(COLLECTION_INSTITUTION_NUMBER), + maskedTelephoneNumber = jsonObject.getStringOrNull(MASKED_TELEPHONE_NUMBER), alternativeReference = jsonObject.getStringOrNull(ALTERNATIVE_REFERENCE), merchantName = jsonObject.getStringOrNull(MERCHANT_NAME), + merchantReference = jsonObject.getStringOrNull(MERCHANT_REFERENCE), url = jsonObject.getStringOrNull(URL), downloadUrl = jsonObject.getStringOrNull(DOWNLOAD_URL), + instructionsUrl = jsonObject.getStringOrNull(INSTRUCTIONS_URL), ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionComponentEvent.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionComponentEvent.kt index ccf9038c4f..5f72dbf593 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionComponentEvent.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionComponentEvent.kt @@ -13,11 +13,16 @@ import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails +import com.adyen.checkout.core.PermissionHandlerCallback @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) sealed class ActionComponentEvent : ComponentEvent { class ActionDetails(val data: ActionComponentData) : ActionComponentEvent() class Error(val error: ComponentError) : ActionComponentEvent() + class PermissionRequest( + val requiredPermission: String, + val permissionCallback: PermissionHandlerCallback + ) : ActionComponentEvent() } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -28,9 +33,19 @@ fun ((PaymentComponentEvent) -> Unit).toActionCallback(): (ActionComponen is ActionComponentEvent.ActionDetails -> { this(PaymentComponentEvent.ActionDetails(actionComponentEvent.data)) } + is ActionComponentEvent.Error -> { this(PaymentComponentEvent.Error(actionComponentEvent.error)) } + + is ActionComponentEvent.PermissionRequest -> { + this( + PaymentComponentEvent.PermissionRequest( + actionComponentEvent.requiredPermission, + actionComponentEvent.permissionCallback + ) + ) + } } } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionObserverRepository.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionObserverRepository.kt index 7e86fcc38b..967a7a74fd 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionObserverRepository.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ActionObserverRepository.kt @@ -21,9 +21,11 @@ class ActionObserverRepository( private val observerContainer: ObserverContainer = ObserverContainer() ) { + @Suppress("LongParameterList") fun addObservers( detailsFlow: Flow?, exceptionFlow: Flow?, + permissionFlow: Flow?, lifecycleOwner: LifecycleOwner, coroutineScope: CoroutineScope, callback: (ActionComponentEvent) -> Unit, @@ -31,12 +33,21 @@ class ActionObserverRepository( with(observerContainer) { removeObservers() - detailsFlow?.observe(lifecycleOwner, coroutineScope) { - callback(ActionComponentEvent.ActionDetails(it)) + detailsFlow?.observe(lifecycleOwner, coroutineScope) { componentData -> + callback(ActionComponentEvent.ActionDetails(componentData)) } - exceptionFlow?.observe(lifecycleOwner, coroutineScope) { - callback(ActionComponentEvent.Error(ComponentError(it))) + exceptionFlow?.observe(lifecycleOwner, coroutineScope) { exception -> + callback(ActionComponentEvent.Error(ComponentError(exception))) + } + + permissionFlow?.observe(lifecycleOwner, coroutineScope) { requestData -> + callback( + ActionComponentEvent.PermissionRequest( + requestData.requiredPermission, + requestData.permissionCallback + ) + ) } } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/AddressLookupComponent.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/AddressLookupComponent.kt new file mode 100644 index 0000000000..d24ce17976 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/AddressLookupComponent.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/1/2024. + */ + +package com.adyen.checkout.components.core.internal + +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress + +/** + * A Component that performs Address Lookup functionality should implement this interface. + */ +interface AddressLookupComponent { + + /** + * Set a callback that will be triggered to perform address lookup actions. + * + * @param addressLookupCallback The callback that will be triggered to perform address lookup options such as + * query changes, completion of the lookup. + */ + fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) + + /** + * Updates the address options that will be displayed to the shopper in + * [com.adyen.checkout.ui.core.internal.ui.view.AddressLookupView] as part of [CardComponent]. + * + * @param options Address option list to be displayed. + */ + fun updateAddressLookupOptions(options: List) + + /** + * Set the result of address completion call. + * + * @param addressLookupResult The result. + */ + fun setAddressLookupResult(addressLookupResult: AddressLookupResult) +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/AlwaysAvailablePaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/AlwaysAvailablePaymentMethod.kt index dabb494fcc..2fc42278ed 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/AlwaysAvailablePaymentMethod.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/AlwaysAvailablePaymentMethod.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.components.core.internal import android.app.Application import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.PaymentMethod @@ -24,4 +25,13 @@ class AlwaysAvailablePaymentMethod : PaymentMethodAvailabilityCheck > /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ constructor( - protected var shopperLocale: Locale, + protected var shopperLocale: Locale?, protected var environment: Environment, protected var clientKey: String ) { @@ -37,6 +37,26 @@ constructor( } } + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor( + environment: Environment, + clientKey: String + ) : this( + shopperLocale = null, + environment = environment, + clientKey = clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,16 +64,29 @@ constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor( + @Suppress("unused") context: Context, environment: Environment, clientKey: String ) : this( - LocaleUtil.getLocale(context), + null, environment, - clientKey + clientKey, ) + /** + * Allows setting the preferred locale of the shopper. + * + * @param shopperLocale The [Locale] of the shopper. + */ + fun setShopperLocale(shopperLocale: Locale): BuilderT { + this.shopperLocale = shopperLocale + @Suppress("UNCHECKED_CAST") + return this as BuilderT + } + /** * Allows configuring the internal analytics of the library. * @@ -70,6 +103,10 @@ constructor( * * Default is null. * + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * * @param amount Amount of the transaction. */ open fun setAmount(amount: Amount): BuilderT { @@ -89,8 +126,10 @@ constructor( throw CheckoutException("Client key does not match the environment.") } - if (!LocaleUtil.isValidLocale(shopperLocale)) { - throw CheckoutException("Invalid shopper locale: $shopperLocale.") + shopperLocale?.let { + if (!LocaleUtil.isValidLocale(it)) { + throw CheckoutException("Invalid shopper locale: $shopperLocale.") + } } return buildInternal() diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/Component.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/Component.kt index 35a8964be4..084776365a 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/Component.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/Component.kt @@ -13,7 +13,6 @@ import com.adyen.checkout.components.core.internal.ui.ComponentDelegate /** * A [Component] is a class that helps to retrieve or format data related to a part of the Checkout API payment. */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) interface Component { /** * The delegate from this component. diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/Configuration.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/Configuration.kt index 7f72f72514..27524392c7 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/Configuration.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/Configuration.kt @@ -10,9 +10,9 @@ import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) interface Configuration : Parcelable { - val shopperLocale: Locale val environment: Environment val clientKey: String + val shopperLocale: Locale? val analyticsConfiguration: AnalyticsConfiguration? val amount: Amount? } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultActionComponentEventHandler.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultActionComponentEventHandler.kt index ca5505935c..6f56eb60ee 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultActionComponentEventHandler.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultActionComponentEventHandler.kt @@ -10,8 +10,8 @@ package com.adyen.checkout.components.core.internal import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.ActionComponentCallback -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class DefaultActionComponentEventHandler( @@ -19,14 +19,14 @@ class DefaultActionComponentEventHandler( ) : ActionComponentEventHandler { override fun onActionComponentEvent(event: ActionComponentEvent) { - Logger.v(TAG, "Event received $event") + adyenLog(AdyenLogLevel.VERBOSE) { "Event received $event" } when (event) { is ActionComponentEvent.ActionDetails -> actionComponentCallback.onAdditionalDetails(event.data) is ActionComponentEvent.Error -> actionComponentCallback.onError(event.error) + is ActionComponentEvent.PermissionRequest -> actionComponentCallback.onPermissionRequest( + event.requiredPermission, + event.permissionCallback, + ) } } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandler.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandler.kt index 716ee82e6d..2e24ee7f36 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandler.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandler.kt @@ -11,9 +11,9 @@ package com.adyen.checkout.components.core.internal import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.PaymentComponentState +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import kotlinx.coroutines.CoroutineScope @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -29,16 +29,16 @@ class DefaultComponentEventHandler> : ComponentEven @Suppress("UNCHECKED_CAST") val callback = componentCallback as? ComponentCallback ?: throw CheckoutException("Callback must be type of ${ComponentCallback::class.java.canonicalName}") - Logger.v(TAG, "Event received $event") + adyenLog(AdyenLogLevel.VERBOSE) { "Event received $event" } when (event) { is PaymentComponentEvent.ActionDetails -> callback.onAdditionalDetails(event.data) is PaymentComponentEvent.Error -> callback.onError(event.error) is PaymentComponentEvent.StateChanged -> callback.onStateChanged(event.state) is PaymentComponentEvent.Submit -> callback.onSubmit(event.state) + is PaymentComponentEvent.PermissionRequest -> callback.onPermissionRequest( + event.requiredPermission, + event.permissionCallback, + ) } } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/NotAvailablePaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/NotAvailablePaymentMethod.kt index e7e249989c..eba4400c20 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/NotAvailablePaymentMethod.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/NotAvailablePaymentMethod.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.components.core.internal import android.app.Application import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.PaymentMethod @@ -24,4 +25,13 @@ class NotAvailablePaymentMethod : PaymentMethodAvailabilityCheck ) { callback.onAvailabilityResult(false, paymentMethod) } + + override fun isAvailable( + application: Application, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + callback: ComponentAvailableCallback + ) { + callback.onAvailabilityResult(false, paymentMethod) + } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ObserverContainer.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ObserverContainer.kt index 96f26954b2..31bf3bc969 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ObserverContainer.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ObserverContainer.kt @@ -11,8 +11,8 @@ package com.adyen.checkout.components.core.internal import androidx.annotation.RestrictTo import androidx.lifecycle.LifecycleOwner import com.adyen.checkout.components.core.internal.util.mapToCallbackWithLifeCycle -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow @@ -30,7 +30,7 @@ class ObserverContainer { mapToCallbackWithLifeCycle( lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ).also { observerJobs.add(it) } @@ -38,12 +38,8 @@ class ObserverContainer { internal fun removeObservers() { if (observerJobs.isEmpty()) return - Logger.d(TAG, "cleaning up existing observer") + adyenLog(AdyenLogLevel.DEBUG) { "cleaning up existing observer" } observerJobs.forEach { it.cancel() } observerJobs.clear() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentComponentEvent.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentComponentEvent.kt index db53f38039..14c49bd3df 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentComponentEvent.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentComponentEvent.kt @@ -13,6 +13,7 @@ import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails +import com.adyen.checkout.core.PermissionHandlerCallback @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) sealed class PaymentComponentEvent> : ComponentEvent { @@ -28,6 +29,11 @@ sealed class PaymentComponentEvent() + class PermissionRequest>( + val requiredPermission: String, + val permissionCallback: PermissionHandlerCallback + ) : PaymentComponentEvent() + class Submit>( val state: ComponentStateT ) : PaymentComponentEvent() diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentDataRepository.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentDataRepository.kt index 7c8460320f..ea55563544 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentDataRepository.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentDataRepository.kt @@ -21,7 +21,14 @@ class PaymentDataRepository( savedStateHandle[PAYMENT_DATA_KEY] = paymentData } + var nativeRedirectData: String? + get() = savedStateHandle[NATIVE_REDIRECT_DATA] + set(paymentData) { + savedStateHandle[NATIVE_REDIRECT_DATA] = paymentData + } + companion object { private const val PAYMENT_DATA_KEY = "payment_data" + private const val NATIVE_REDIRECT_DATA = "native_redirect_data" } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentMethodAvailabilityCheck.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentMethodAvailabilityCheck.kt index 285b133a3f..6b2f9ba01f 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentMethodAvailabilityCheck.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PaymentMethodAvailabilityCheck.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.components.core.internal import android.app.Application +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.PaymentMethod @@ -18,10 +19,18 @@ import com.adyen.checkout.components.core.PaymentMethod * [Configuration] if not applicable. */ interface PaymentMethodAvailabilityCheck { + fun isAvailable( applicationContext: Application, paymentMethod: PaymentMethod, configuration: ConfigurationT?, callback: ComponentAvailableCallback ) + + fun isAvailable( + application: Application, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + callback: ComponentAvailableCallback + ) } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/PermissionRequestData.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PermissionRequestData.kt new file mode 100644 index 0000000000..a551f67c84 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/PermissionRequestData.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 9/1/2024. + */ + +package com.adyen.checkout.components.core.internal + +import androidx.annotation.RestrictTo +import com.adyen.checkout.core.PermissionHandlerCallback + +/** + * Runtime permission request data + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class PermissionRequestData( + val requiredPermission: String, + val permissionCallback: PermissionHandlerCallback +) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsService.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsService.kt index e190f0437a..429b375b2f 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsService.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsService.kt @@ -13,24 +13,26 @@ import com.adyen.checkout.components.core.internal.data.model.AnalyticsSetupRequ import com.adyen.checkout.components.core.internal.data.model.AnalyticsSetupResponse import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.post +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class AnalyticsService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { internal suspend fun setupAnalytics( request: AnalyticsSetupRequest, clientKey: String, - ): AnalyticsSetupResponse = withContext(Dispatchers.IO) { + ): AnalyticsSetupResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v3/analytics", queryParameters = mapOf("clientKey" to clientKey), body = request, requestSerializer = AnalyticsSetupRequest.SERIALIZER, - responseSerializer = AnalyticsSetupResponse.SERIALIZER + responseSerializer = AnalyticsSetupResponse.SERIALIZER, ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepository.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepository.kt index 6de3590329..b9557b82a7 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepository.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepository.kt @@ -13,8 +13,8 @@ import androidx.annotation.VisibleForTesting import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel.ALL import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel.NONE -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -38,7 +38,7 @@ class DefaultAnalyticsRepository( } if (state != State.Uninitialized) return state = State.InProgress - Logger.v(TAG, "Setting up analytics") + adyenLog(AdyenLogLevel.VERBOSE) { "Setting up analytics" } runSuspendCatching { val analyticsSetupRequest = with(analyticsRepositoryData) { @@ -55,10 +55,12 @@ class DefaultAnalyticsRepository( val response = analyticsService.setupAnalytics(analyticsSetupRequest, analyticsRepositoryData.clientKey) checkoutAttemptId = response.checkoutAttemptId state = State.Ready - Logger.v(TAG, "Analytics setup call successful") + adyenLog(AdyenLogLevel.VERBOSE) { "Analytics setup call successful" } }.onFailure { e -> state = State.Failed - Logger.e(TAG, "Failed to send analytics setup call - ${e::class.simpleName}: ${e.message}") + adyenLog(AdyenLogLevel.ERROR) { + "Failed to send analytics setup call - ${e::class.simpleName}: ${e.message}" + } } } @@ -68,7 +70,6 @@ class DefaultAnalyticsRepository( } companion object { - private val TAG = LogUtil.getTag() @VisibleForTesting internal const val CHECKOUT_ATTEMPT_ID_FOR_DISABLED_ANALYTICS = "do-not-track" diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultPublicKeyRepository.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultPublicKeyRepository.kt index 29f24403f7..6204cf2cca 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultPublicKeyRepository.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/DefaultPublicKeyRepository.kt @@ -9,9 +9,9 @@ package com.adyen.checkout.components.core.internal.data.api import androidx.annotation.RestrictTo +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -23,7 +23,7 @@ class DefaultPublicKeyRepository( environment: Environment, clientKey: String ): Result = runSuspendCatching { - Logger.d(TAG, "fetching publicKey from API") + adyenLog(AdyenLogLevel.DEBUG) { "fetching publicKey from API" } retryOnFailure(CONNECTION_RETRIES) { publicKeyService.getPublicKey(clientKey).publicKey @@ -47,7 +47,6 @@ class DefaultPublicKeyRepository( } companion object { - private val TAG = LogUtil.getTag() private const val CONNECTION_RETRIES = 3 } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusRepository.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusRepository.kt index 25a0ce0b38..59ee6a1f39 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusRepository.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusRepository.kt @@ -9,11 +9,10 @@ package com.adyen.checkout.components.core.internal.data.api import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.components.core.internal.data.model.OrderStatusRequest import com.adyen.checkout.components.core.internal.data.model.OrderStatusResponse -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -22,19 +21,15 @@ class OrderStatusRepository( ) { suspend fun getOrderStatus( - configuration: Configuration, + clientKey: String, orderData: String ): Result = runSuspendCatching { - Logger.d(TAG, "Getting order status") + adyenLog(AdyenLogLevel.DEBUG) { "Getting order status" } val request = OrderStatusRequest(orderData) orderStatusService.getOrderStatus( request, - configuration.clientKey + clientKey, ) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusService.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusService.kt index b01210c88b..5fe4a1f9de 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusService.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/OrderStatusService.kt @@ -13,24 +13,26 @@ import com.adyen.checkout.components.core.internal.data.model.OrderStatusRequest import com.adyen.checkout.components.core.internal.data.model.OrderStatusResponse import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.post +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class OrderStatusService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { internal suspend fun getOrderStatus( request: OrderStatusRequest, clientKey: String - ): OrderStatusResponse = withContext(Dispatchers.IO) { + ): OrderStatusResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/order/status", queryParameters = mapOf("clientKey" to clientKey), body = request, requestSerializer = OrderStatusRequest.SERIALIZER, - responseSerializer = OrderStatusResponse.SERIALIZER + responseSerializer = OrderStatusResponse.SERIALIZER, ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/PublicKeyService.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/PublicKeyService.kt index e014be49f1..15e4c1365e 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/PublicKeyService.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/PublicKeyService.kt @@ -12,17 +12,19 @@ import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.internal.data.model.PublicKeyResponse import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.get +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class PublicKeyService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { internal suspend fun getPublicKey( clientKey: String - ): PublicKeyResponse = withContext(Dispatchers.IO) { + ): PublicKeyResponse = withContext(coroutineDispatcher) { httpClient.get( "v1/clientKeys/$clientKey", PublicKeyResponse.SERIALIZER diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/StatusRepository.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/StatusRepository.kt index c32655671c..ba23f8de61 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/StatusRepository.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/data/api/StatusRepository.kt @@ -12,9 +12,10 @@ import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.internal.data.model.StatusRequest import com.adyen.checkout.components.core.internal.data.model.StatusResponse import com.adyen.checkout.components.core.internal.util.StatusResponseUtils -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.currentCoroutineContext @@ -37,9 +38,10 @@ interface StatusRepository { } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -class DefaultStatusRepository constructor( +class DefaultStatusRepository( private val statusService: StatusService, private val clientKey: String, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : StatusRepository { private var delay: Long = 0 @@ -73,7 +75,7 @@ class DefaultStatusRepository constructor( ) } - private suspend fun fetchStatus(paymentData: String) = withContext(Dispatchers.IO) { + private suspend fun fetchStatus(paymentData: String) = withContext(coroutineDispatcher) { runSuspendCatching { statusService.checkStatus(clientKey, StatusRequest(paymentData)) } @@ -89,22 +91,22 @@ class DefaultStatusRepository constructor( delay = POLLING_DELAY_FAST true } + elapsedTime <= maxPollingDuration -> { delay = POLLING_DELAY_SLOW true } + else -> false } } override fun refreshStatus(paymentData: String) { - Logger.v(TAG, "refreshStatus") + adyenLog(AdyenLogLevel.VERBOSE) { "refreshStatus" } refreshFlow.tryEmit(paymentData) } companion object { - private val TAG = LogUtil.getTag() - private val POLLING_DELAY_FAST = TimeUnit.SECONDS.toMillis(2) private val POLLING_DELAY_SLOW = TimeUnit.SECONDS.toMillis(10) private val POLLING_THRESHOLD = TimeUnit.SECONDS.toMillis(60) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/ActionComponentProvider.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/ActionComponentProvider.kt index 3e7cae34e0..ca266a0abe 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/ActionComponentProvider.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/ActionComponentProvider.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.ActionComponent import com.adyen.checkout.components.core.internal.Configuration @@ -28,6 +29,106 @@ interface ActionComponentProvider< DelegateT : ActionDelegate > : ComponentProvider { + //region CheckoutConfiguration + + /** + * Get an [ActionComponent]. + * + * @param fragment The Fragment to associate the lifecycle. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [ActionComponent]. + * @param key The key to use to identify the [ActionComponent]. + * + * NOTE: By default only one [ActionComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [ActionComponent]s in the same lifecycle. + * + * @return The Component + */ + fun get( + fragment: Fragment, + checkoutConfiguration: CheckoutConfiguration, + callback: ActionComponentCallback, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = fragment, + viewModelStoreOwner = fragment, + lifecycleOwner = fragment.viewLifecycleOwner, + application = fragment.requireApplication(), + checkoutConfiguration = checkoutConfiguration, + callback = callback, + key = key, + ) + } + + /** + * Get an [ActionComponent]. + * + * @param activity The Activity to associate the lifecycle. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [ActionComponent]. + * @param key The key to use to identify the [ActionComponent]. + * + * NOTE: By default only one [ActionComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [ActionComponent]s in the same lifecycle. + * + * @return The Component + */ + fun get( + activity: ComponentActivity, + checkoutConfiguration: CheckoutConfiguration, + callback: ActionComponentCallback, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = activity, + viewModelStoreOwner = activity, + lifecycleOwner = activity, + application = activity.application, + checkoutConfiguration = checkoutConfiguration, + callback = callback, + key = key, + ) + } + + /** + * Get an [ActionComponent]. + * + * @param savedStateRegistryOwner The owner of the SavedStateRegistry, normally an Activity or Fragment. + * @param viewModelStoreOwner A scope that owns ViewModelStore, normally an Activity or Fragment. + * @param lifecycleOwner The lifecycle owner, normally an Activity or Fragment. + * @param application Your main application class. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [ActionComponent]. + * @param key The key to use to identify the [ActionComponent]. + * + * NOTE: By default only one [ActionComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [ActionComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + checkoutConfiguration: CheckoutConfiguration, + callback: ActionComponentCallback, + key: String? = null, + ): ComponentT + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getDelegate( + checkoutConfiguration: CheckoutConfiguration, + savedStateHandle: SavedStateHandle, + application: Application + ): DelegateT + + //endregion + + //region Component specific configuration + /** * Get an [ActionComponent]. * @@ -54,7 +155,7 @@ interface ActionComponentProvider< application = fragment.requireApplication(), configuration = configuration, callback = callback, - key = key + key = key, ) } @@ -84,7 +185,7 @@ interface ActionComponentProvider< application = activity.application, configuration = configuration, callback = callback, - key = key + key = key, ) } @@ -115,12 +216,7 @@ interface ActionComponentProvider< key: String? = null, ): ComponentT - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - fun getDelegate( - configuration: ConfigurationT, - savedStateHandle: SavedStateHandle, - application: Application, - ): DelegateT + //endregion /** * Checks if the provided component can handle a given action. diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/PaymentComponentProvider.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/PaymentComponentProvider.kt index d3da63fbbf..8a87510ee6 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/PaymentComponentProvider.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/PaymentComponentProvider.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentComponentState @@ -29,6 +30,117 @@ interface PaymentComponentProvider< > : ComponentProvider { + //region CheckoutConfiguration + + /** + * Get a [PaymentComponent]. + * + * @param fragment The Fragment to associate the lifecycle. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + fragment: Fragment, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + callback: ComponentCallbackT, + order: Order? = null, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = fragment, + viewModelStoreOwner = fragment, + lifecycleOwner = fragment.viewLifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = fragment.requireApplication(), + componentCallback = callback, + order = order, + key = key, + ) + } + + /** + * Get a [PaymentComponent]. + * + * @param activity The Activity to associate the lifecycle. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + activity: ComponentActivity, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + callback: ComponentCallbackT, + order: Order? = null, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = activity, + viewModelStoreOwner = activity, + lifecycleOwner = activity, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = activity.application, + componentCallback = callback, + order = order, + key = key, + ) + } + + /** + * Get a [PaymentComponent]. + * + * @param savedStateRegistryOwner The owner of the SavedStateRegistry, normally an Activity or Fragment. + * @param viewModelStoreOwner A scope that owns ViewModelStore, normally an Activity or Fragment. + * @param lifecycleOwner The lifecycle owner, normally an Activity or Fragment. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param application Your main application class. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallbackT, + order: Order?, + key: String?, + ): ComponentT + + //endregion + + //region Payment method specific configuration + /** * Get a [PaymentComponent]. * @@ -134,6 +246,8 @@ interface PaymentComponentProvider< key: String?, ): ComponentT + //endregion + /** * Checks if the provided component can handle a given payment method. */ diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/StoredPaymentComponentProvider.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/StoredPaymentComponentProvider.kt index 5167df31e8..14842a2c0b 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/StoredPaymentComponentProvider.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/provider/StoredPaymentComponentProvider.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentComponentState @@ -28,6 +29,117 @@ interface StoredPaymentComponentProvider< ComponentCallbackT : ComponentCallback > { + //region CheckoutConfiguration + + /** + * Get a [PaymentComponent] with a stored payment method. + * + * @param fragment The Fragment to associate the lifecycle. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + fragment: Fragment, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + callback: ComponentCallbackT, + order: Order? = null, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = fragment, + viewModelStoreOwner = fragment, + lifecycleOwner = fragment.viewLifecycleOwner, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = fragment.requireApplication(), + componentCallback = callback, + order = order, + key = key, + ) + } + + /** + * Get a [PaymentComponent] with a stored payment method. + * + * @param activity The Activity to associate the lifecycle. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param callback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + activity: ComponentActivity, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + callback: ComponentCallbackT, + order: Order? = null, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = activity, + viewModelStoreOwner = activity, + lifecycleOwner = activity, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = activity.application, + componentCallback = callback, + order = order, + key = key, + ) + } + + /** + * Get a [PaymentComponent] with a stored payment method. + * + * @param savedStateRegistryOwner The owner of the SavedStateRegistry, normally an Activity or Fragment. + * @param viewModelStoreOwner A scope that owns ViewModelStore, normally an Activity or Fragment. + * @param lifecycleOwner The lifecycle owner, normally an Activity or Fragment. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param application Your main application class. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param order An [Order] in case of an ongoing partial payment flow. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallbackT, + order: Order?, + key: String?, + ): ComponentT + + //endregion + + //region Payment method specific configuration + /** * Get a [PaymentComponent] with a stored payment method. * @@ -133,6 +245,8 @@ interface StoredPaymentComponentProvider< key: String?, ): ComponentT + //endregion + /** * Checks if the provided component can handle a given stored payment method. */ diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/PermissionRequestingDelegate.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/PermissionRequestingDelegate.kt new file mode 100644 index 0000000000..9d9caf28ca --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/PermissionRequestingDelegate.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 8/1/2024. + */ + +package com.adyen.checkout.components.core.internal.ui + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.PermissionRequestData +import kotlinx.coroutines.flow.Flow + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface PermissionRequestingDelegate { + + val permissionFlow: Flow +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/AddressInputModel.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/AddressInputModel.kt new file mode 100644 index 0000000000..ab6b4904fc --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/AddressInputModel.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 7/2/2024. + */ + +package com.adyen.checkout.components.core.internal.ui.model + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class AddressInputModel( + var postalCode: String = "", + var street: String = "", + var stateOrProvince: String = "", + var houseNumberOrName: String = "", + var apartmentSuite: String = "", + var city: String = "", + var country: String = "", +) { + + fun set(addressInputModel: AddressInputModel) { + postalCode = addressInputModel.postalCode + street = addressInputModel.street + stateOrProvince = addressInputModel.stateOrProvince + houseNumberOrName = addressInputModel.houseNumberOrName + apartmentSuite = addressInputModel.apartmentSuite + city = addressInputModel.city + country = addressInputModel.country + } + + /** + * Reset the data. + * + * Note: This method is called when country is changed and that's the reason [country] field + * does not get reset. + */ + fun reset() { + postalCode = "" + street = "" + stateOrProvince = "" + houseNumberOrName = "" + apartmentSuite = "" + city = "" + } + + /** + * Reset the data. + * + * Note: This method is called from address lookup and all the form needs to be reset. + */ + fun resetAll() { + country = "" + postalCode = "" + street = "" + stateOrProvince = "" + houseNumberOrName = "" + apartmentSuite = "" + city = "" + } + + val isEmpty + get() = postalCode.isEmpty() && + street.isEmpty() && + stateOrProvince.isEmpty() && + houseNumberOrName.isEmpty() && + apartmentSuite.isEmpty() && + city.isEmpty() && + country.isEmpty() +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParams.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParams.kt index 02ead71ea0..a04faa123d 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParams.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParams.kt @@ -9,17 +9,9 @@ package com.adyen.checkout.components.core.internal.ui.model import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.core.Environment -import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class ButtonComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, -) : ComponentParams, ButtonParams +) : ComponentParams by commonComponentParams, ButtonParams diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapper.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapper.kt index d019037425..9e73f85978 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapper.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapper.kt @@ -1,57 +1,32 @@ package com.adyen.checkout.components.core.internal.ui.model import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.Configuration +import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class ButtonComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - configuration: Configuration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + componentConfiguration: Configuration? ): ButtonComponentParams { - return configuration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } - - private fun Configuration.mapToParamsInternal(): ButtonComponentParams { - return ButtonComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - isSubmitButtonVisible = (this as? ButtonConfiguration)?.isSubmitButtonVisible ?: true + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, ) - } - - private fun ButtonComponentParams.override( - overrideComponentParams: ComponentParams? - ): ButtonComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount - ) - } - - private fun ButtonComponentParams.override( - sessionParams: SessionParams? = null - ): ButtonComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, + return ButtonComponentParams( + commonComponentParams = commonComponentParamsMapperData.commonComponentParams, + isSubmitButtonVisible = (componentConfiguration as? ButtonConfiguration)?.isSubmitButtonVisible ?: true, ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParams.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParams.kt new file mode 100644 index 0000000000..f85fad88cf --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParams.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 15/2/2024. + */ + +package com.adyen.checkout.components.core.internal.ui.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.core.Environment +import java.util.Locale + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CommonComponentParams( + override val shopperLocale: Locale, + override val environment: Environment, + override val clientKey: String, + override val analyticsParams: AnalyticsParams, + override val isCreatedByDropIn: Boolean, + override val amount: Amount?, +) : ComponentParams diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParamsMapper.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParamsMapper.kt new file mode 100644 index 0000000000..25c456e0a8 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParamsMapper.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 15/2/2024. + */ + +package com.adyen.checkout.components.core.internal.ui.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.CheckoutConfiguration +import java.util.Locale + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class CommonComponentParamsMapper { + + fun mapToParams( + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + ): CommonComponentParamsMapperData { + val sessionParams: SessionParams? = dropInOverrideParams?.sessionParams ?: componentSessionParams + val commonComponentParams = CommonComponentParams( + shopperLocale = checkoutConfiguration.shopperLocale ?: sessionParams?.shopperLocale ?: deviceLocale, + environment = sessionParams?.environment ?: checkoutConfiguration.environment, + clientKey = sessionParams?.clientKey ?: checkoutConfiguration.clientKey, + analyticsParams = AnalyticsParams(checkoutConfiguration.analyticsConfiguration), + isCreatedByDropIn = dropInOverrideParams != null, + amount = sessionParams?.amount + ?: dropInOverrideParams?.amount + ?: checkoutConfiguration.amount, + ) + return CommonComponentParamsMapperData(commonComponentParams, sessionParams) + } +} diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParamsMapperData.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParamsMapperData.kt new file mode 100644 index 0000000000..d4ec0fae87 --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/CommonComponentParamsMapperData.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 15/2/2024. + */ + +package com.adyen.checkout.components.core.internal.ui.model + +import androidx.annotation.RestrictTo + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class CommonComponentParamsMapperData( + val commonComponentParams: CommonComponentParams, + val sessionParams: SessionParams?, +) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/DropInOverrideParams.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/DropInOverrideParams.kt new file mode 100644 index 0000000000..d12822e18c --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/DropInOverrideParams.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 24/1/2024. + */ + +package com.adyen.checkout.components.core.internal.ui.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.Amount + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class DropInOverrideParams( + val amount: Amount?, + val sessionParams: SessionParams?, +) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParams.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParams.kt index 1fcc53294f..a01cf7722f 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParams.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParams.kt @@ -9,16 +9,8 @@ package com.adyen.checkout.components.core.internal.ui.model import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.core.Environment -import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class GenericComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, -) : ComponentParams + private val commonComponentParams: CommonComponentParams, +) : ComponentParams by commonComponentParams diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapper.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapper.kt index 5a38ef2c39..4a9d8ee4f9 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapper.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapper.kt @@ -9,55 +9,30 @@ package com.adyen.checkout.components.core.internal.ui.model import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.CheckoutConfiguration +import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class GenericComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - configuration: Configuration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, ): GenericComponentParams { - return configuration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } - - private fun Configuration.mapToParamsInternal(): GenericComponentParams { - return GenericComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount - ) - } - - private fun GenericComponentParams.override( - overrideComponentParams: ComponentParams? - ): GenericComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, ) - } + val commonComponentParams = commonComponentParamsMapperData.commonComponentParams - private fun GenericComponentParams.override( - sessionParams: SessionParams? = null - ): GenericComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, + return GenericComponentParams( + commonComponentParams = commonComponentParams, ) } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt index 8370d89d39..69810b9941 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/ui/model/SessionParams.kt @@ -10,11 +10,25 @@ package com.adyen.checkout.components.core.internal.ui.model import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.core.Environment +import java.util.Locale +/** + * Object that holds values set during sessions setup call. + * [SessionParams] values by default should have higher priority than values set in client side configurations. + * Otherwise it can cause server error, since specific configuration is not enabled, but it is being used. + * + * Only for some specific cases, if they do not cause server error, client side configurations can have higher priority + * than [SessionParams] values. + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class SessionParams( + val environment: Environment, + val clientKey: String, val enableStoreDetails: Boolean?, val installmentConfiguration: SessionInstallmentConfiguration?, + val showRemovePaymentMethodButton: Boolean?, val amount: Amount?, val returnUrl: String?, + val shopperLocale: Locale?, ) diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/AmountFormat.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/AmountFormat.kt index d88359f257..507e6950fa 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/AmountFormat.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/AmountFormat.kt @@ -10,16 +10,15 @@ package com.adyen.checkout.components.core.internal.util import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.CheckoutCurrency +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import java.math.BigDecimal import java.util.Currency import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) object AmountFormat { - private val TAG = LogUtil.getTag() /** * Convert an [Amount] to its corresponding [BigDecimal] value. @@ -50,18 +49,16 @@ object AmountFormat { val checkoutCurrency = CheckoutCurrency.find(normalizedCurrencyCode) return checkoutCurrency.fractionDigits } catch (e: CheckoutException) { - Logger.e( - tag = TAG, - msg = "$normalizedCurrencyCode is an unsupported currency. Falling back to information from " + - "java.util.Currency.", - tr = e - ) + adyenLog(AdyenLogLevel.ERROR, e) { + "$normalizedCurrencyCode is an unsupported currency. Falling back to information from " + + "java.util.Currency." + } } return try { val currency = Currency.getInstance(normalizedCurrencyCode) currency.defaultFractionDigits.coerceAtLeast(0) } catch (e: IllegalArgumentException) { - Logger.e(TAG, "Could not determine fraction digits for $normalizedCurrencyCode", e) + adyenLog(AdyenLogLevel.ERROR, e) { "Could not determine fraction digits for $normalizedCurrencyCode" } 0 } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/CheckoutConfigurationMarker.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/CheckoutConfigurationMarker.kt new file mode 100644 index 0000000000..9519705edf --- /dev/null +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/CheckoutConfigurationMarker.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 29/1/2024. + */ + +package com.adyen.checkout.components.core.internal.util + +import androidx.annotation.RestrictTo + +@DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +annotation class CheckoutConfigurationMarker diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/DateUtils.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/DateUtils.kt index 7ff3224295..52ac38faf5 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/DateUtils.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/DateUtils.kt @@ -9,8 +9,8 @@ package com.adyen.checkout.components.core.internal.util import androidx.annotation.RestrictTo -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import java.text.DateFormat import java.text.ParseException import java.text.SimpleDateFormat @@ -21,8 +21,6 @@ import java.util.Locale object DateUtils { private const val DEFAULT_INPUT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" - private val TAG = LogUtil.getTag() - @JvmStatic fun parseDateToView(month: String, year: String): String { // Refactor this to DateFormat if we need to localize. @@ -32,6 +30,7 @@ object DateUtils { /** * Convert to server date format. */ + @Suppress("unused") @JvmStatic fun toServerDateFormat(calendar: Calendar): String { val serverDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) @@ -51,7 +50,7 @@ object DateUtils { dateFormat.parse(date) true } catch (e: ParseException) { - Logger.e(TAG, "Provided date $date does not match the given format $format") + adyenLog(AdyenLogLevel.ERROR) { "Provided date $date does not match the given format $format" } false } } @@ -67,15 +66,30 @@ object DateUtils { date: String, shopperLocale: Locale, inputFormat: String = DEFAULT_INPUT_DATE_FORMAT - ): String? { - return try { - val inputSimpleFormat = SimpleDateFormat(inputFormat, shopperLocale) - val outputSimpleFormat = DateFormat.getDateInstance(DateFormat.SHORT, shopperLocale) - val parsedDate = inputSimpleFormat.parse(date) - parsedDate?.let { outputSimpleFormat.format(it) } - } catch (e: ParseException) { - Logger.e(TAG, "Provided date $date does not match the given format $inputFormat") - null - } + ): String? = try { + val inputSimpleFormat = SimpleDateFormat(inputFormat, shopperLocale) + val outputSimpleFormat = DateFormat.getDateInstance(DateFormat.SHORT, shopperLocale) + val parsedDate = inputSimpleFormat.parse(date) + parsedDate?.let { outputSimpleFormat.format(it) } + } catch (e: ParseException) { + adyenLog(AdyenLogLevel.ERROR, e) { "Provided date $date does not match the given format $inputFormat" } + null + } + + /** + * Format date time to provided date time pattern. + * + * @param calendar Calendar instance to be formatted + * @param pattern Date pattern + */ + fun formatDateToString( + calendar: Calendar, + pattern: String = DEFAULT_INPUT_DATE_FORMAT + ): String? = try { + val formatter = SimpleDateFormat(pattern, Locale.US) + formatter.format(calendar.time) + } catch (e: IllegalArgumentException) { + adyenLog(AdyenLogLevel.ERROR, e) { "Provided pattern $pattern is invalid" } + null } } diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt index 779468153a..944e8ea62f 100644 --- a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt +++ b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/GenericPaymentMethod.kt @@ -14,7 +14,7 @@ import org.json.JSONException import org.json.JSONObject @Parcelize -class GenericPaymentMethod( +data class GenericPaymentMethod( override var type: String?, override var checkoutAttemptId: String?, ) : PaymentMethodDetails() { diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/ActionComponentCallbackTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/ActionComponentCallbackTest.kt new file mode 100644 index 0000000000..4528dbae92 --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/ActionComponentCallbackTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 2/2/2024. + */ + +package com.adyen.checkout.components.core + +import com.adyen.checkout.core.PermissionHandlerCallback +import org.junit.jupiter.api.Test +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +internal class ActionComponentCallbackTest { + + private val componentCallback = TestActionComponentCallback() + + @Test + fun `when onPermissionRequest is called, then onPermissionRequestNotHandled is invoked`() { + val requiredPermission = "permission" + val permissionCallback = mock() + + componentCallback.onPermissionRequest(requiredPermission, permissionCallback) + + verify(permissionCallback).onPermissionRequestNotHandled(eq(requiredPermission)) + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/AddressInputModelTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/AddressInputModelTest.kt new file mode 100644 index 0000000000..2ee1249b2b --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/AddressInputModelTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 31/1/2024. + */ + +package com.adyen.checkout.components.core + +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class AddressInputModelTest { + + @Test + fun `when set is called address input model should copy all the information from the given model`() { + val addressInputModel = AddressInputModel() + + val addressInputModelFrom = AddressInputModel( + postalCode = "postalCode", + street = "street", + city = "city", + country = "country", + stateOrProvince = "stateOrProvince", + apartmentSuite = "apartmentSuite", + houseNumberOrName = "houseNumberOrName", + ) + + addressInputModel.set(addressInputModelFrom) + + assertEquals(addressInputModelFrom, addressInputModel) + } + + @Test + fun `when reset is called address input model should reset all fields except country`() { + val addressInputModel = AddressInputModel( + postalCode = "postalCode", + street = "street", + city = "city", + country = "country", + stateOrProvince = "stateOrProvince", + apartmentSuite = "apartmentSuite", + houseNumberOrName = "houseNumberOrName", + ) + + addressInputModel.reset() + + assertEquals("", addressInputModel.postalCode) + assertEquals("", addressInputModel.street) + assertEquals("", addressInputModel.city) + assertEquals("", addressInputModel.stateOrProvince) + assertEquals("", addressInputModel.apartmentSuite) + assertEquals("", addressInputModel.houseNumberOrName) + assertEquals("country", addressInputModel.country) + } + + @Test + fun `when resetAll is called address input model should reset all fields`() { + val addressInputModel = AddressInputModel( + postalCode = "postalCode", + street = "street", + city = "city", + country = "country", + stateOrProvince = "stateOrProvince", + apartmentSuite = "apartmentSuite", + houseNumberOrName = "houseNumberOrName", + ) + + addressInputModel.resetAll() + + assertEquals("", addressInputModel.postalCode) + assertEquals("", addressInputModel.street) + assertEquals("", addressInputModel.city) + assertEquals("", addressInputModel.stateOrProvince) + assertEquals("", addressInputModel.apartmentSuite) + assertEquals("", addressInputModel.houseNumberOrName) + assertEquals("", addressInputModel.country) + } + + @Test + fun `when all fields are empty isEmpty should return true`() { + val addressInputModel = AddressInputModel( + postalCode = "", + street = "", + city = "", + country = "", + stateOrProvince = "", + apartmentSuite = "", + houseNumberOrName = "", + ) + + assertTrue(addressInputModel.isEmpty) + } + + @Test + fun `when all fields are not empty isEmpty should return false`() { + val addressInputModel = AddressInputModel( + postalCode = "postalCode", + street = "street", + city = "city", + country = "country", + stateOrProvince = "stateOrProvince", + apartmentSuite = "apartmentSuite", + houseNumberOrName = "houseNumberOrName", + ) + + assertFalse(addressInputModel.isEmpty) + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/ButtonTestConfiguration.kt b/components-core/src/test/java/com/adyen/checkout/components/core/ButtonTestConfiguration.kt index b8cd4cd41f..ea6c275406 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/ButtonTestConfiguration.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/ButtonTestConfiguration.kt @@ -1,6 +1,5 @@ package com.adyen.checkout.components.core -import android.content.Context import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder @@ -11,7 +10,7 @@ import java.util.Locale @Parcelize class ButtonTestConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -19,22 +18,15 @@ class ButtonTestConfiguration private constructor( override val isSubmitButtonVisible: Boolean?, ) : Configuration, ButtonConfiguration { - class Builder : BaseConfigurationBuilder, ButtonConfigurationBuilder { + class Builder( + shopperLocale: Locale?, + environment: Environment, + clientKey: String + ) : BaseConfigurationBuilder(shopperLocale, environment, clientKey), + ButtonConfigurationBuilder { private var isSubmitButtonVisible: Boolean? = null - constructor(context: Context, environment: Environment, clientKey: String) : super( - context, - environment, - clientKey - ) - - constructor( - shopperLocale: Locale, - environment: Environment, - clientKey: String - ) : super(shopperLocale, environment, clientKey) - override fun setSubmitButtonVisible(isSubmitButtonVisible: Boolean): ButtonConfigurationBuilder { this.isSubmitButtonVisible = isSubmitButtonVisible return this diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/CheckoutConfigurationTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/CheckoutConfigurationTest.kt new file mode 100644 index 0000000000..061d87d382 --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/CheckoutConfigurationTest.kt @@ -0,0 +1,81 @@ +package com.adyen.checkout.components.core + +import android.os.Parcel +import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.core.Environment +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.util.Locale + +@RunWith(RobolectricTestRunner::class) +internal class CheckoutConfigurationTest { + + @Test + fun `when parcelized, then it must be correctly deparcelized`() { + val testConfiguration = TestConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build() + val testActionConfiguration = TestConfiguration.Builder(Locale.CHINA, Environment.APSE, LIVE_CLIENT_KEY).build() + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + addActionConfiguration(testActionConfiguration) + } + + assertCorrectParcelization(checkoutConfiguration, testConfiguration, testActionConfiguration) + } + + @Test + fun `when parcelized with required fields only, then it must be correctly deparcelized`() { + val testConfiguration = TestConfiguration.Builder(null, Environment.TEST, TEST_CLIENT_KEY).build() + val testActionConfiguration = TestConfiguration.Builder(null, Environment.APSE, LIVE_CLIENT_KEY).build() + val checkoutConfiguration = CheckoutConfiguration( + Environment.TEST, + TEST_CLIENT_KEY, + ) { + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + addActionConfiguration(testActionConfiguration) + } + + assertCorrectParcelization(checkoutConfiguration, testConfiguration, testActionConfiguration) + } + + private fun assertCorrectParcelization( + checkoutConfiguration: CheckoutConfiguration, + paymentMethodConfiguration: Configuration, + actionConfiguration: Configuration + ) { + val parcel = Parcel.obtain() + checkoutConfiguration.writeToParcel(parcel, 0) + // Reset the parcel for reading + parcel.setDataPosition(0) + + val deparcelized = CheckoutConfiguration.createFromParcel(parcel) + assertConfigurationEquals(checkoutConfiguration, deparcelized) + + val deparcelizedPmConfig = deparcelized.getConfiguration(TEST_CONFIGURATION_KEY) + assertConfigurationEquals(paymentMethodConfiguration, requireNotNull(deparcelizedPmConfig)) + + val deparcelizedActionConfig = deparcelized.getActionConfiguration(TestConfiguration::class.java) + assertConfigurationEquals(actionConfiguration, requireNotNull(deparcelizedActionConfig)) + } + + private fun assertConfigurationEquals(expected: Configuration, actual: Configuration) { + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + } + + companion object { + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + private const val LIVE_CLIENT_KEY = "live_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/ComponentCallbackTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/ComponentCallbackTest.kt new file mode 100644 index 0000000000..c37acff3b0 --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/ComponentCallbackTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 2/2/2024. + */ + +package com.adyen.checkout.components.core + +import com.adyen.checkout.core.PermissionHandlerCallback +import org.junit.jupiter.api.Test +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +internal class ComponentCallbackTest { + + private val componentCallback = TestComponentCallback() + + @Test + fun `when onPermissionRequest is called, then onPermissionRequestNotHandled is invoked`() { + val requiredPermission = "permission" + val permissionCallback = mock() + + componentCallback.onPermissionRequest(requiredPermission, permissionCallback) + + verify(permissionCallback).onPermissionRequestNotHandled(eq(requiredPermission)) + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/PaymentComponentDataTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/PaymentComponentDataTest.kt new file mode 100644 index 0000000000..8ad5ca6d8d --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/PaymentComponentDataTest.kt @@ -0,0 +1,104 @@ +package com.adyen.checkout.components.core + +import com.adyen.checkout.components.core.paymentmethod.GenericPaymentMethod +import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class PaymentComponentDataTest { + + @Test + fun `when serializing, then all fields should be serialized correctly`() { + val paymentMethod = GenericPaymentMethod("type", "checkoutAttemptId") + val order = OrderRequest("pspReference", "orderData") + val amount = Amount("EUR", 1L) + val billingAddress = Address(city = "city") + val deliveryAddress = Address(street = "street") + val shopperName = ShopperName(firstName = "firstName") + val installments = Installments("plan", 2) + val request = PaymentComponentData( + paymentMethod = paymentMethod, + order = order, + amount = amount, + storePaymentMethod = true, + shopperReference = "shopperReference", + billingAddress = billingAddress, + deliveryAddress = deliveryAddress, + shopperName = shopperName, + telephoneNumber = "telephoneNumber", + shopperEmail = "shopperEmail", + dateOfBirth = "dateOfBirth", + socialSecurityNumber = "socialSecurityNumber", + installments = installments, + supportNativeRedirect = true, + ) + + val actual = PaymentComponentData.SERIALIZER.serialize(request) + + val expected = JSONObject() + .put("paymentMethod", PaymentMethodDetails.SERIALIZER.serialize(paymentMethod)) + .put("order", OrderRequest.SERIALIZER.serialize(order)) + .put("amount", Amount.SERIALIZER.serialize(amount)) + .put("storePaymentMethod", true) + .put("shopperReference", "shopperReference") + .put("billingAddress", Address.SERIALIZER.serialize(billingAddress)) + .put("deliveryAddress", Address.SERIALIZER.serialize(deliveryAddress)) + .put("shopperName", ShopperName.SERIALIZER.serialize(shopperName)) + .put("telephoneNumber", "telephoneNumber") + .put("shopperEmail", "shopperEmail") + .put("dateOfBirth", "dateOfBirth") + .put("socialSecurityNumber", "socialSecurityNumber") + .put("installments", Installments.SERIALIZER.serialize(installments)) + .put("supportNativeRedirect", true) + + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun `when deserializing, then all fields should be deserializing correctly`() { + val paymentMethod = GenericPaymentMethod("type", "checkoutAttemptId") + val order = OrderRequest("pspReference", "orderData") + val amount = Amount("EUR", 1L) + val billingAddress = Address(city = "city") + val deliveryAddress = Address(street = "street") + val shopperName = ShopperName(firstName = "firstName") + val installments = Installments("plan", 2) + val response = JSONObject() + .put("paymentMethod", PaymentMethodDetails.SERIALIZER.serialize(paymentMethod)) + .put("order", OrderRequest.SERIALIZER.serialize(order)) + .put("amount", Amount.SERIALIZER.serialize(amount)) + .put("storePaymentMethod", true) + .put("shopperReference", "shopperReference") + .put("billingAddress", Address.SERIALIZER.serialize(billingAddress)) + .put("deliveryAddress", Address.SERIALIZER.serialize(deliveryAddress)) + .put("shopperName", ShopperName.SERIALIZER.serialize(shopperName)) + .put("telephoneNumber", "telephoneNumber") + .put("shopperEmail", "shopperEmail") + .put("dateOfBirth", "dateOfBirth") + .put("socialSecurityNumber", "socialSecurityNumber") + .put("installments", Installments.SERIALIZER.serialize(installments)) + .put("supportNativeRedirect", true) + + val actual = PaymentComponentData.SERIALIZER.deserialize(response) + + val expected = PaymentComponentData( + paymentMethod = paymentMethod, + order = order, + amount = amount, + storePaymentMethod = true, + shopperReference = "shopperReference", + billingAddress = billingAddress, + deliveryAddress = deliveryAddress, + shopperName = shopperName, + telephoneNumber = "telephoneNumber", + shopperEmail = "shopperEmail", + dateOfBirth = "dateOfBirth", + socialSecurityNumber = "socialSecurityNumber", + installments = installments, + supportNativeRedirect = true, + ) + + assertEquals(expected, actual) + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/TestActionComponentCallback.kt b/components-core/src/test/java/com/adyen/checkout/components/core/TestActionComponentCallback.kt new file mode 100644 index 0000000000..67298064a2 --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/TestActionComponentCallback.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 2/2/2024. + */ + +package com.adyen.checkout.components.core + +internal class TestActionComponentCallback : ActionComponentCallback { + override fun onAdditionalDetails(actionComponentData: ActionComponentData) { + // Not necessary + } + + override fun onError(componentError: ComponentError) { + // Not necessary + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/TestComponentCallback.kt b/components-core/src/test/java/com/adyen/checkout/components/core/TestComponentCallback.kt new file mode 100644 index 0000000000..260e2cacc0 --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/TestComponentCallback.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 2/2/2024. + */ + +package com.adyen.checkout.components.core + +internal class TestComponentCallback : ComponentCallback { + override fun onSubmit(state: TestComponentState) { + // Not necessary + } + + override fun onAdditionalDetails(actionComponentData: ActionComponentData) { + // Not necessary + } + + override fun onError(componentError: ComponentError) { + // Not necessary + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/TestConfiguration.kt b/components-core/src/test/java/com/adyen/checkout/components/core/TestConfiguration.kt index 9299e3ceae..d4db3aa3d8 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/TestConfiguration.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/TestConfiguration.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.components.core -import android.content.Context import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration import com.adyen.checkout.core.Environment @@ -17,26 +16,15 @@ import java.util.Locale @Parcelize class TestConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, override val amount: Amount? ) : Configuration { - class Builder : BaseConfigurationBuilder { - - constructor(context: Context, environment: Environment, clientKey: String) : super( - context, - environment, - clientKey - ) - - constructor( - shopperLocale: Locale, - environment: Environment, - clientKey: String - ) : super(shopperLocale, environment, clientKey) + class Builder(shopperLocale: Locale?, environment: Environment, clientKey: String) : + BaseConfigurationBuilder(shopperLocale, environment, clientKey) { override fun buildInternal(): TestConfiguration { return TestConfiguration( @@ -44,7 +32,7 @@ class TestConfiguration private constructor( environment = environment, clientKey = clientKey, analyticsConfiguration = analyticsConfiguration, - amount = amount + amount = amount, ) } } diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/action/RedirectActionTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/action/RedirectActionTest.kt new file mode 100644 index 0000000000..7f691294ba --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/action/RedirectActionTest.kt @@ -0,0 +1,56 @@ +package com.adyen.checkout.components.core.action + +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class RedirectActionTest { + + @Test + fun `when serializing, then all fields should be serialized correctly`() { + val request = RedirectAction( + type = "type", + paymentData = "paymentData", + paymentMethodType = "paymentMethodType", + method = "method", + url = "url", + nativeRedirectData = "nativeRedirectData", + ) + + val actual = RedirectAction.SERIALIZER.serialize(request) + + val expected = JSONObject() + .put("type", "type") + .put("paymentData", "paymentData") + .put("paymentMethodType", "paymentMethodType") + .put("method", "method") + .put("url", "url") + .put("nativeRedirectData", "nativeRedirectData") + + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun `when deserializing, then all fields should be deserializing correctly`() { + val response = JSONObject() + .put("type", "type") + .put("paymentData", "paymentData") + .put("paymentMethodType", "paymentMethodType") + .put("method", "method") + .put("url", "url") + .put("nativeRedirectData", "nativeRedirectData") + + val actual = RedirectAction.SERIALIZER.deserialize(response) + + val expected = RedirectAction( + type = "type", + paymentData = "paymentData", + paymentMethodType = "paymentMethodType", + method = "method", + url = "url", + nativeRedirectData = "nativeRedirectData", + ) + + assertEquals(expected, actual) + } +} diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandlerTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandlerTest.kt index e8e179bcae..dff754e32b 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandlerTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/DefaultComponentEventHandlerTest.kt @@ -14,17 +14,20 @@ import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.TestComponentState -import com.adyen.checkout.core.AdyenLogger +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify +@ExtendWith(LoggingExtension::class) internal class DefaultComponentEventHandlerTest { private lateinit var componentEventHandler: DefaultComponentEventHandler> @@ -32,7 +35,6 @@ internal class DefaultComponentEventHandlerTest { @BeforeEach fun beforeEach() { componentEventHandler = DefaultComponentEventHandler() - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -44,7 +46,7 @@ internal class DefaultComponentEventHandlerTest { assertThrows { componentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - object : BaseComponentCallback {} + object : BaseComponentCallback {}, ) } } @@ -56,7 +58,7 @@ internal class DefaultComponentEventHandlerTest { componentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.ActionDetails(actionData), - callback + callback, ) verify(callback).onAdditionalDetails(actionData) @@ -69,7 +71,7 @@ internal class DefaultComponentEventHandlerTest { componentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Error(error), - callback + callback, ) verify(callback).onError(error) @@ -82,12 +84,26 @@ internal class DefaultComponentEventHandlerTest { componentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.StateChanged(state), - callback + callback, ) verify(callback).onStateChanged(state) } + @Test + fun `is PermissionRequested, then required permission and permission callback should be propagated`() { + val callback = mock>>() + val requiredPermission = "permission" + val permissionCallback = mock() + + componentEventHandler.onPaymentComponentEvent( + PaymentComponentEvent.PermissionRequest(requiredPermission, permissionCallback), + callback, + ) + + verify(callback).onPermissionRequest(eq(requiredPermission), eq(permissionCallback)) + } + @Test fun `is Submit, then state should be propagated`() { val callback = mock>>() @@ -95,7 +111,7 @@ internal class DefaultComponentEventHandlerTest { componentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(state), - callback + callback, ) verify(callback).onSubmit(state) diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt index 7cece7b550..8d2f680eab 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/AnalyticsMapperTest.kt @@ -119,7 +119,7 @@ internal class AnalyticsMapperTest { ) val expected = AnalyticsSetupRequest( - version = "5.2.0", + version = "5.3.0", channel = "android", platform = "android", locale = "en_US", diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepositoryTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepositoryTest.kt index b8eab6a044..0d24ac5405 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepositoryTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultAnalyticsRepositoryTest.kt @@ -12,9 +12,8 @@ import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.internal.data.model.AnalyticsSetupResponse import com.adyen.checkout.components.core.internal.data.model.AnalyticsSource import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.exception.HttpException -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -37,7 +36,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class DefaultAnalyticsRepositoryTest( @Mock private val analyticsService: AnalyticsService, ) { @@ -48,10 +47,9 @@ internal class DefaultAnalyticsRepositoryTest( @BeforeEach fun before() = runTest { - AdyenLogger.setLogLevel(Logger.NONE) analyticsRepository = getDefaultAnalyticsRepository() whenever( - analyticsService.setupAnalytics(any(), any()) + analyticsService.setupAnalytics(any(), any()), ) doReturn AnalyticsSetupResponse(checkoutAttemptId = TEST_CHECKOUT_ATTEMPT_ID) } @@ -126,7 +124,7 @@ internal class DefaultAnalyticsRepositoryTest( @Test fun `and level is set to ALL then call is made`() = runTest { analyticsRepository = getDefaultAnalyticsRepository( - level = AnalyticsParamsLevel.ALL + level = AnalyticsParamsLevel.ALL, ) analyticsRepository.setupAnalytics() @@ -137,7 +135,7 @@ internal class DefaultAnalyticsRepositoryTest( @Test fun `and level is set to NONE then call is not made`() = runTest { analyticsRepository = getDefaultAnalyticsRepository( - level = AnalyticsParamsLevel.NONE + level = AnalyticsParamsLevel.NONE, ) analyticsRepository.setupAnalytics() @@ -148,14 +146,14 @@ internal class DefaultAnalyticsRepositoryTest( @Test fun `and level is set to NONE then checkoutAttemptId is not set`() = runTest { analyticsRepository = getDefaultAnalyticsRepository( - level = AnalyticsParamsLevel.NONE + level = AnalyticsParamsLevel.NONE, ) analyticsRepository.setupAnalytics() assertEquals( DefaultAnalyticsRepository.CHECKOUT_ATTEMPT_ID_FOR_DISABLED_ANALYTICS, - analyticsRepository.getCheckoutAttemptId() + analyticsRepository.getCheckoutAttemptId(), ) } } diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt index 3936168dca..ab0af5e49f 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/ButtonComponentParamsMapperTest.kt @@ -1,9 +1,12 @@ package com.adyen.checkout.components.core.internal.ui.model import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel import com.adyen.checkout.components.core.ButtonTestConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.core.Environment -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments.arguments @@ -12,110 +15,218 @@ import java.util.Locale internal class ButtonComponentParamsMapperTest { - @Test - fun `when parent configuration is null then params should match the component configuration`() { - val componentConfiguration = getButtonConfigurationBuilder().build() + private val buttonComponentParamsMapper = ButtonComponentParamsMapper(CommonComponentParamsMapper()) - val params = ButtonComponentParamsMapper(null, null).mapToParams(componentConfiguration, null) + @Test + fun `when drop-in override params are null then params should match the component configuration`() { + val configuration = createCheckoutConfiguration() + + val params = buttonComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + ) val expected = getButtonComponentParams() - Assertions.assertEquals(expected, params) + assertEquals(expected, params) } @Test - fun `when parent configuration is set then parent configuration fields should override component configuration fields`() { - val componentConfiguration = getButtonConfigurationBuilder().build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override component configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "EUR", - value = 49_00L - ) - ) - - val params = ButtonComponentParamsMapper(overrideParams, null).mapToParams( - componentConfiguration, - null + value = 49_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + val testConfiguration = ButtonTestConfiguration.Builder(Locale.CANADA, Environment.TEST, TEST_CLIENT_KEY_1) + .setAmount(Amount("USD", 1L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + } + + val dropInOverrideParams = DropInOverrideParams(Amount("USD", 123L), null) + val params = buttonComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), ) - val expected = ButtonComponentParams( + val expected = getButtonComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "EUR", - value = 49_00L + currency = "USD", + value = 123L, ), - isSubmitButtonVisible = true + isSubmitButtonVisible = true, ) - Assertions.assertEquals(expected, params) + assertEquals(expected, params) } @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val buttonConfiguration = getButtonConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getButtonComponentParams().copy(amount = it) } - - val params = ButtonComponentParamsMapper(overrideParams, null).mapToParams( - buttonConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ) + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + val params = buttonComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), ) - val expected = getButtonComponentParams().copy(amount = expectedValue) + val expected = getButtonComponentParams( + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) - Assertions.assertEquals(expected, params) + assertEquals(expected, params) } - private fun getButtonConfigurationBuilder(): ButtonTestConfiguration.Builder { - return ButtonTestConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, ) + + val params = buttonComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + ) + + val expected = getButtonComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) } - private fun getButtonComponentParams(): ButtonComponentParams { + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = buttonComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + ) + + val expected = getButtonComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: ButtonTestConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + val testConfiguration = ButtonTestConfiguration.Builder(shopperLocale, environment, clientKey) + .apply(configuration) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, + ) + + @Suppress("LongParameterList") + private fun getButtonComponentParams( + shopperLocale: Locale = DEVICE_LOCALE, + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + isCreatedByDropIn: Boolean = false, + amount: Amount? = null, + isSubmitButtonVisible: Boolean = true, + ): ButtonComponentParams { return ButtonComponentParams( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false, - amount = null, - isSubmitButtonVisible = true, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), + isSubmitButtonVisible = isSubmitButtonVisible, ) } companion object { + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun amountSource() = listOf( @@ -124,5 +235,14 @@ internal class ButtonComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt index a5b38fa79f..bb86aa2bbb 100644 --- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/ui/model/GenericComponentParamsMapperTest.kt @@ -9,6 +9,9 @@ package com.adyen.checkout.components.core.internal.ui.model import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.TestConfiguration import com.adyen.checkout.core.Environment import org.junit.jupiter.api.Assertions.assertEquals @@ -20,11 +23,13 @@ import java.util.Locale internal class GenericComponentParamsMapperTest { + private val genericComponentParamsMapper = GenericComponentParamsMapper(CommonComponentParamsMapper()) + @Test - fun `when parent configuration is null then params should match the component configuration`() { - val componentConfiguration = getTestConfigurationBuilder().build() + fun `when drop-in override params are null then params should match the component configuration`() { + val configuration = createCheckoutConfiguration() - val params = GenericComponentParamsMapper(null, null).mapToParams(componentConfiguration, null) + val params = genericComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) val expected = getGenericComponentParams() @@ -32,42 +37,41 @@ internal class GenericComponentParamsMapperTest { } @Test - fun `when parent configuration is set then parent configuration fields should override component configuration fields`() { - val componentConfiguration = TestConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 - ).build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override component configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "EUR", - value = 49_00L + value = 49_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + val testConfiguration = TestConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, ) - ) + .setAmount(Amount("USD", 1L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + } - val params = GenericComponentParamsMapper(overrideParams, null).mapToParams( - componentConfiguration, - null - ) + val dropInOverrideParams = DropInOverrideParams(Amount("CAD", 123L), null) + val params = genericComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, dropInOverrideParams, null) - val expected = GenericComponentParams( + val expected = getGenericComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "EUR", - value = 49_00L - ) + currency = "CAD", + value = 123L, + ), ) assertEquals(expected, params) @@ -75,57 +79,148 @@ internal class GenericComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val testConfiguration = getTestConfigurationBuilder() - .setAmount(configurationValue) - .build() + val testConfiguration = createCheckoutConfiguration(configurationValue) - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getGenericComponentParams().copy(amount = it) } + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } - val params = GenericComponentParamsMapper(overrideParams, null).mapToParams( + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val params = genericComponentParamsMapper.mapToParams( testConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ) + DEVICE_LOCALE, + dropInOverrideParams, + sessionParams, ) - val expected = getGenericComponentParams().copy(amount = expectedValue) + val expected = getGenericComponentParams( + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) assertEquals(expected, params) } - private fun getTestConfigurationBuilder(): TestConfiguration.Builder { - return TestConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = genericComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getGenericComponentParams( + shopperLocale = expectedValue, ) + + assertEquals(expected, params) } - private fun getGenericComponentParams(): GenericComponentParams { + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = genericComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getGenericComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + val testConfiguration = TestConfiguration.Builder(shopperLocale, environment, clientKey) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, + ) + + @Suppress("LongParameterList") + private fun getGenericComponentParams( + shopperLocale: Locale = DEVICE_LOCALE, + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + isCreatedByDropIn: Boolean = false, + amount: Amount? = null, + ): GenericComponentParams { return GenericComponentParams( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false, - amount = null + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), ) } companion object { + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun amountSource() = listOf( @@ -134,5 +229,14 @@ internal class GenericComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/DateUtilsTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/DateUtilsTest.kt new file mode 100644 index 0000000000..7599bd8fbc --- /dev/null +++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/DateUtilsTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 22/1/2024. + */ + +package com.adyen.checkout.components.core.internal.util + +import com.adyen.checkout.test.LoggingExtension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.Calendar +import java.util.Locale + +@ExtendWith(LoggingExtension::class) +class DateUtilsTest { + + @ParameterizedTest + @MethodSource("parseDateToView") + fun testParseDateToView(month: String, year: String, expectedFormattedDate: String) { + val formattedDate = DateUtils.parseDateToView(month, year) + + assertEquals(expectedFormattedDate, formattedDate) + } + + @ParameterizedTest + @MethodSource("toServerDateFormat") + fun testToServerDateFormat(calendar: Calendar, expectedFormattedDate: String) { + val formattedDate = DateUtils.toServerDateFormat(calendar) + + assertEquals(expectedFormattedDate, formattedDate) + } + + @ParameterizedTest + @MethodSource("matchesFormat") + fun testMatchesFormat(date: String, format: String, expectedResult: Boolean) { + val result = DateUtils.matchesFormat(date, format) + + assertEquals(expectedResult, result) + } + + @ParameterizedTest + @MethodSource("formatStringDate") + fun testFormatStringDate(date: String, shopperLocale: Locale, inputFormat: String, expectedFormattedDate: String?) { + val formattedDate = DateUtils.formatStringDate(date, shopperLocale, inputFormat) + + assertEquals(expectedFormattedDate, formattedDate) + } + + @ParameterizedTest + @MethodSource("formatDateToString") + fun testFormatDateToString(calendar: Calendar, pattern: String, expectedFormattedDate: String?) { + val formattedDate = DateUtils.formatDateToString(calendar, pattern) + + assertEquals(expectedFormattedDate, formattedDate) + } + + companion object { + @JvmStatic + fun parseDateToView() = listOf( + // month, year, expectedFormattedDate + arguments("1", "2021", "1/21"), + arguments("02", "2020", "02/20"), + arguments("12", "2023", "12/23"), + ) + + @JvmStatic + fun toServerDateFormat() = listOf( + // date, expectedFormattedDate + arguments(getCalendar(2023, 1, 7), "2023-02-07"), + arguments(getCalendar(2024, 0, 1), "2024-01-01"), + arguments(getCalendar(2024, 11, 1), "2024-12-01"), + arguments(getCalendar(2019, 0, 1, 0, 0, 0), "2019-01-01"), + arguments(getCalendar(2019, 0, 1, 20, 30, 30), "2019-01-01"), + ) + + @JvmStatic + fun matchesFormat() = listOf( + // date, format, expectedResult + arguments("2023-02-07'T'20:00:00", "yyyyMMdd", false), + arguments("2023-02-07", "yyyyMMdd", false), + arguments("2023-02-07", "yyyyMM", false), + arguments("20233112", "yyyyMMdd", false), + arguments("233112", "yyMMdd", false), + arguments("2331", "yyMM", false), + arguments("20230207T20:00:00", "yyyyMMdd'T'HH:mm:ss", true), + arguments("20230207T20:00:00", "yyyyMMdd", true), + arguments("230207T20:00:00", "yyMMdd", true), + arguments("20230207", "yyyyMMdd", true), + arguments("202302", "yyyyMM", true), + arguments("230201", "yyMMdd", true), + arguments("2302", "yyMM", true), + ) + + @JvmStatic + fun formatStringDate() = listOf( + // date, shopperLocale, inputFormat, expectedResult + arguments("2023-02-07'T'20:00:00", Locale.US, "yyyy-MM-dd'T'HH:mm:ss", null), + arguments("2024-02-01", Locale.US, "yyyy-MM-dd", "2/1/24"), + arguments("230207", Locale.US, "yyMMdd", "2/7/23"), + arguments("231302", Locale.US, "yyMMdd", "1/2/24"), + arguments("2024-02-01", Locale.UK, "yy-MM-dd", "01/02/2024"), + arguments("231302", Locale.UK, "yyMMdd", "02/01/2024"), + arguments("2024-02-01", Locale.JAPAN, "yy-MM-dd", "2024/02/01"), + arguments("2024-02-01", Locale.CHINA, "yy-MM-dd", "2024/2/1"), + arguments("2024-02-01", Locale.CANADA, "yy-MM-dd", "2024-02-01"), + arguments("2024-02-01", Locale.FRANCE, "yy-MM-dd", "01/02/2024"), + ) + + @JvmStatic + fun formatDateToString() = listOf( + // date, pattern, expectedFormattedDate + arguments(getCalendar(2024, 0, 5, 20, 10, 5), "yyyy-MM-dd'T'HH:mm:ss", "2024-01-05T20:10:05"), + arguments(getCalendar(2020, 11, 31, 23, 59, 59), "yyyy-MM-dd'T'HH:mm:ss", "2020-12-31T23:59:59"), + arguments(getCalendar(2019, 0, 1, 0, 0, 0), "yyyy-MM-dd'T'HH:mm:ss", "2019-01-01T00:00:00"), + arguments(getCalendar(2019, 0, 1, 0, 0, 0), "yyyyMMdd'T'HHmmss", "20190101T000000"), + arguments(getCalendar(2019, 1, 1, 0, 0, 0), "yyyyMMdd", "20190201"), + arguments(getCalendar(2024, 0, 5, 20, 10, 5), "xxxxxxxxxxxx", null), + ) + + @JvmStatic + fun getCalendar(year: Int, month: Int, date: Int): Calendar = + Calendar.getInstance().apply { + set(year, month, date) + } + + @JvmStatic + @Suppress("LongParameterList") + fun getCalendar(year: Int, month: Int, date: Int, hourOfDay: Int, minute: Int, second: Int): Calendar = + Calendar.getInstance().apply { + set(year, month, date, hourOfDay, minute, second) + } + } +} diff --git a/config/checkstyle/checkstyle-rules-adyen.xml b/config/checkstyle/checkstyle-rules-adyen.xml deleted file mode 100644 index 9e3a3dfcfe..0000000000 --- a/config/checkstyle/checkstyle-rules-adyen.xml +++ /dev/null @@ -1,363 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/config/gradle/codeQuality.gradle b/config/gradle/codeQuality.gradle index 23623ed21a..ceaecc6d27 100644 --- a/config/gradle/codeQuality.gradle +++ b/config/gradle/codeQuality.gradle @@ -9,5 +9,3 @@ apply from: "${rootDir}/config/gradle/jacoco.gradle" apply from: "${rootDir}/config/gradle/ktlint.gradle" apply from: "${rootDir}/config/gradle/detekt.gradle" - -check.dependsOn "ktlint", "detekt" diff --git a/config/gradle/jacoco.gradle b/config/gradle/jacoco.gradle index 192a15b1c5..1e1ee9d25d 100644 --- a/config/gradle/jacoco.gradle +++ b/config/gradle/jacoco.gradle @@ -141,24 +141,32 @@ ext.coverageExclusions = [ '**/*DropInService.*', '**/*Factory.*', '**/*ViewProvider.*', - '**/AdyenLogger.*', + '**/AdyenLogKt*', '**/BuildUtils.*', + '**/CheckCompileOnlyKt*', + '**/ComposeExtensions*.*', '**/ContextExtensions*.*', + '**/DropIn$*.*', + '**/DropIn.*', '**/DropInExt*.*', '**/FileDownloader*.*', '**/FragmentExtensions*.*', + '**/FragmentProvider$*.*', + '**/FragmentProvider.*', '**/ImageLoadingExtensions*.*', '**/ImageLoadingExtensions.*', '**/ImageSaver.*', '**/InstallmentFilter.*', '**/LazyArguments*.*', '**/LifecycleExtensions*.*', - '**/LogUtil.*', - '**/Logger.*', + '**/LogcatLogger.*', '**/PdfOpener.*', + '**/ResultExtKt*', + '**/RunCompileOnlyKt*', '**/ViewExtensions*.*', '**/ViewModelExt*.*', - // Example app is not applicable - 'com/adyen/checkout/example/**' + // Example app and test-core are not applicable + 'com/adyen/checkout/example/**', + 'com/adyen/checkout/test/**' ] diff --git a/config/gradle/ktlint.gradle b/config/gradle/ktlint.gradle index 33b1d735a4..1bde8ae02b 100644 --- a/config/gradle/ktlint.gradle +++ b/config/gradle/ktlint.gradle @@ -20,20 +20,25 @@ dependencies { ktlint "com.pinterest.ktlint:ktlint-cli:$ktlint_version" } -task ktlint(type: JavaExec) { +tasks.register("ktlint", JavaExec) { description = "Check Kotlin code style." group = "verification" classpath = configurations.ktlint - main = "com.pinterest.ktlint.Main" - args "src/**/main/**/*.kt", "--reporter=plain", "--reporter=checkstyle,output=${project.buildDir}/reports/klint/klint-results.xml" + mainClass = "com.pinterest.ktlint.Main" + args "src/**/main/**/*.kt", "**.kts", "!**/build/**" } -task ktlintFormat(type: JavaExec) { +tasks.named("check") { + dependsOn tasks.named("ktlint") +} + +tasks.register("ktlintFormat", JavaExec) { description = "Fix Kotlin code style deviations." group = "formatting" classpath = configurations.ktlint - main = "com.pinterest.ktlint.Main" - args "-F", "src/**/*.kt" + mainClass = "com.pinterest.ktlint.Main" + jvmArgs "--add-opens=java.base/java.lang=ALL-UNNAMED" + args "-F", "src/**/*.kt", "**.kts", "!**/build/**" } diff --git a/config/gradle/sonarcloud.gradle b/config/gradle/sonarcloud.gradle index 1d8737a96e..6a91b0f72b 100644 --- a/config/gradle/sonarcloud.gradle +++ b/config/gradle/sonarcloud.gradle @@ -21,6 +21,12 @@ project(":example-app") { } } +project(":test-core") { + sonar { + skipProject = true + } +} + subprojects { apply plugin: 'org.sonarqube' diff --git a/config/module/template/build.gradle b/config/module/template/build.gradle index faa526f20b..9440cc3aa3 100644 --- a/config/module/template/build.gradle +++ b/config/module/template/build.gradle @@ -21,20 +21,8 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' consumerProguardFiles "consumer-rules.pro" } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } } dependencies { - //Tests - testImplementation testLibraries.junit5 - androidTestImplementation testLibraries.androidTest - androidTestImplementation testLibraries.espresso + } diff --git a/config/pmd/pmd-rules.xml b/config/pmd/pmd-rules.xml deleted file mode 100644 index a31cc2423d..0000000000 --- a/config/pmd/pmd-rules.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - Pmd rules - - .*/R.java - .*/gen/.* - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/convenience-stores-jp/build.gradle b/convenience-stores-jp/build.gradle index 74b80afa77..5c0c334f07 100644 --- a/convenience-stores-jp/build.gradle +++ b/convenience-stores-jp/build.gradle @@ -32,4 +32,8 @@ android { dependencies { api project(':econtext') + + testImplementation project(':test-core') + testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito } diff --git a/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfiguration.kt b/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfiguration.kt index beecdbd543..7b09b795e8 100644 --- a/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfiguration.kt +++ b/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.econtext.internal.EContextConfiguration import kotlinx.parcelize.Parcelize @@ -23,7 +26,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class ConvenienceStoresJPConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -37,6 +40,22 @@ class ConvenienceStoresJPConfiguration private constructor( */ class Builder : EContextConfiguration.Builder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,14 +63,15 @@ class ConvenienceStoresJPConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -76,3 +96,38 @@ class ConvenienceStoresJPConfiguration private constructor( } } } + +fun CheckoutConfiguration.convenienceStoresJP( + configuration: @CheckoutConfigurationMarker ConvenienceStoresJPConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = ConvenienceStoresJPConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ECONTEXT_STORES, config) + return this +} + +fun CheckoutConfiguration.getConvenienceStoresJPConfiguration(): ConvenienceStoresJPConfiguration? { + return getConfiguration(PaymentMethodTypes.ECONTEXT_STORES) +} + +internal fun ConvenienceStoresJPConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ECONTEXT_STORES, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/internal/provider/ConvenienceStoresJPComponentProvider.kt b/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/internal/provider/ConvenienceStoresJPComponentProvider.kt index 10c3f8d04b..34f88479cd 100644 --- a/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/internal/provider/ConvenienceStoresJPComponentProvider.kt +++ b/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/internal/provider/ConvenienceStoresJPComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.conveniencestoresjp.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.ConvenienceStoresJPPaymentMethod import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPComponent import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPComponentState import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPConfiguration +import com.adyen.checkout.conveniencestoresjp.getConvenienceStoresJPConfiguration +import com.adyen.checkout.conveniencestoresjp.toCheckoutConfiguration import com.adyen.checkout.econtext.internal.provider.EContextComponentProvider import com.adyen.checkout.econtext.internal.ui.EContextDelegate class ConvenienceStoresJPComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : EContextComponentProvider< ConvenienceStoresJPComponent, ConvenienceStoresJPConfiguration, ConvenienceStoresJPPaymentMethod, - ConvenienceStoresJPComponentState + ConvenienceStoresJPComponentState, >( componentClass = ConvenienceStoresJPComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -57,7 +57,7 @@ constructor( delegate = delegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) } @@ -65,6 +65,14 @@ constructor( return ConvenienceStoresJPPaymentMethod() } + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): ConvenienceStoresJPConfiguration? { + return checkoutConfiguration.getConvenienceStoresJPConfiguration() + } + + override fun getCheckoutConfiguration(configuration: ConvenienceStoresJPConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } + override fun getSupportedPaymentMethods(): List { return ConvenienceStoresJPComponent.PAYMENT_METHOD_TYPES } diff --git a/convenience-stores-jp/src/test/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfigurationTest.kt b/convenience-stores-jp/src/test/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfigurationTest.kt new file mode 100644 index 0000000000..cbde7da61e --- /dev/null +++ b/convenience-stores-jp/src/test/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.conveniencestoresjp + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class ConvenienceStoresJPConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + convenienceStoresJP { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getConvenienceStoresJPConfiguration() + + val expected = ConvenienceStoresJPConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = ConvenienceStoresJPConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualJpConfig = actual.getConvenienceStoresJPConfiguration() + assertEquals(config.shopperLocale, actualJpConfig?.shopperLocale) + assertEquals(config.environment, actualJpConfig?.environment) + assertEquals(config.clientKey, actualJpConfig?.clientKey) + assertEquals(config.amount, actualJpConfig?.amount) + assertEquals(config.analyticsConfiguration, actualJpConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualJpConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/dependencies.gradle b/dependencies.gradle index 8e986e6324..92212156db 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -16,15 +16,15 @@ ext { // just for example app, don't need to increment version_code = 1 // The version_name format is "major.minor.patch(-(alpha|beta|rc)[0-9]{2}){0,1}" (e.g. 3.0.0, 3.1.1-alpha04 or 3.1.4-rc01 etc). - version_name = "5.2.0" + version_name = "5.3.0" // Build Script android_gradle_plugin_version = '8.2.0' - kotlin_version = '1.9.21' + kotlin_version = '1.9.22' detekt_gradle_plugin_version = "1.23.4" dokka_version = "1.9.10" - hilt_version = "2.49" - compose_compiler_version = '1.5.7' + hilt_version = "2.50" + compose_compiler_version = '1.5.8' // Code quality detekt_version = "1.23.4" @@ -33,18 +33,18 @@ ext { sonarqube_version = '4.4.1.3373' // Android Dependencies - annotation_version = "1.7.0" + annotation_version = "1.7.1" appcompat_version = "1.6.1" browser_version = "1.7.0" coroutines_version = "1.6.4" fragment_version = "1.6.2" lifecycle_version = "2.5.1" - material_version = "1.10.0" + material_version = "1.11.0" recyclerview_version = "1.3.2" constraintlayout_version = '2.1.4' // Compose Dependencies - compose_activity_version = '1.8.1' + compose_activity_version = '1.8.2' compose_bom_version = '2023.10.01' compose_hilt_version = '1.1.0' compose_viewmodel_version = '2.6.2' @@ -60,7 +60,7 @@ ext { wechat_pay_version = "6.8.0" // Example app - leak_canary_version = '2.12' + leak_canary_version = '2.13' moshi_adapters_version = '1.14.0' moshi_kotlin_adapter_version = '1.14.0' okhttp_logging_version = "4.12.0" @@ -127,6 +127,7 @@ ext { ], okhttp : "com.squareup.okhttp3:okhttp:$okhttp_version", okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$okhttp_logging_version", + parcelize : "org.jetbrains.kotlin:kotlin-parcelize-runtime:$kotlin_version", retrofit : [ "com.squareup.retrofit2:retrofit:$retrofit2_version", "com.squareup.retrofit2:converter-moshi:$retrofit2_version" diff --git a/docs/ADDRESS_LOOKUP.md b/docs/ADDRESS_LOOKUP.md new file mode 100644 index 0000000000..1b221e22c9 --- /dev/null +++ b/docs/ADDRESS_LOOKUP.md @@ -0,0 +1,17 @@ +## Enabling Address Lookup Functionality +- You can enable this feature by setting your address configuration to lookup while building your card configuration as follows: + ```kotlin + CheckoutConfiguration(environment = environment, clientKey = clientKey) { + card { + setAddressConfiguration(AddressConfiguration.Lookup()) + } + } + ``` +## Integrating with Address Lookup Functionality +- If you're integrating with Drop-in: + - Implement the mandatory `onAddressLookupQueryChanged(query: String)` callback and optional `onAddressLookupCompletion(lookupAddress: LookupAddress)` callback. + - Pass the result of these actions by using `AddressLookupDropInServiceResult` class. +- If you're integrating with standalone `CardComponent`: + - Set `AddressLookupCallback` via `CardComponent.setAddressLookupCallback(AddressLookupCallback)` function to receive the related events. + - Pass the result of these actions by calling `CardComponent.setAddressLookupResult(addressLookupResult: AddressLookupResult)`. + - Delegate back pressed event to `CardComponent` by calling `CardComponent.handleBackPress()` which returns true if the back press is handled by Adyen SDK and false otherwise. diff --git a/docs/UI_CUSTOMIZATION.md b/docs/UI_CUSTOMIZATION.md index d5bc5ac9d2..18e620d8d9 100644 --- a/docs/UI_CUSTOMIZATION.md +++ b/docs/UI_CUSTOMIZATION.md @@ -117,10 +117,12 @@ It is possible to change text in the SDK by overriding string resources. To over Payment method names in the payment method list can be overridden with a configuration object: ```kotlin -DropInConfiguration.Builder(shopperLocale, environment, clientKey) - .overridePaymentMethodName(PaymentMethodTypes.SCHEME, "Credit cards") - .overridePaymentMethodName(PaymentMethodTypes.GIFTCARD, "Specific gift card") - .build() +CheckoutConfiguration(shopperLocale, environment, clientKey) { + dropIn { + overridePaymentMethodName(PaymentMethodTypes.SCHEME, "Credit cards") + overridePaymentMethodName(PaymentMethodTypes.GIFTCARD, "Specific gift card") + } +} ``` If you cannot find a certain string in the code base, then check whether it is coming from the Checkout API. Make sure you localize these strings by sending the correct [shopperLocale](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions#request-shopperLocale). diff --git a/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayConfiguration.kt b/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayConfiguration.kt index d3ec92724a..7ca83938d3 100644 --- a/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayConfiguration.kt +++ b/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayConfiguration.kt @@ -11,6 +11,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -23,7 +26,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class DotpayConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -39,6 +42,22 @@ class DotpayConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -46,14 +65,15 @@ class DotpayConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -80,3 +100,38 @@ class DotpayConfiguration private constructor( } } } + +fun CheckoutConfiguration.dotpay( + configuration: @CheckoutConfigurationMarker DotpayConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = DotpayConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.DOTPAY, config) + return this +} + +fun CheckoutConfiguration.getDotpayConfiguration(): DotpayConfiguration? { + return getConfiguration(PaymentMethodTypes.DOTPAY) +} + +internal fun DotpayConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.DOTPAY, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/dotpay/src/main/java/com/adyen/checkout/dotpay/internal/provider/DotpayComponentProvider.kt b/dotpay/src/main/java/com/adyen/checkout/dotpay/internal/provider/DotpayComponentProvider.kt index e2a3be54f1..d3e8c6bae9 100644 --- a/dotpay/src/main/java/com/adyen/checkout/dotpay/internal/provider/DotpayComponentProvider.kt +++ b/dotpay/src/main/java/com/adyen/checkout/dotpay/internal/provider/DotpayComponentProvider.kt @@ -11,28 +11,28 @@ package com.adyen.checkout.dotpay.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.DotpayPaymentMethod import com.adyen.checkout.dotpay.DotpayComponent import com.adyen.checkout.dotpay.DotpayComponentState import com.adyen.checkout.dotpay.DotpayConfiguration +import com.adyen.checkout.dotpay.getDotpayConfiguration +import com.adyen.checkout.dotpay.toCheckoutConfiguration import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate class DotpayComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider( componentClass = DotpayComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -57,4 +57,12 @@ constructor( override fun createPaymentMethod() = DotpayPaymentMethod() override fun getSupportedPaymentMethods(): List = DotpayComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): DotpayConfiguration? { + return checkoutConfiguration.getDotpayConfiguration() + } + + override fun getCheckoutConfiguration(configuration: DotpayConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/dotpay/src/test/java/com/adyen/checkout/dotpay/DotpayConfigurationTest.kt b/dotpay/src/test/java/com/adyen/checkout/dotpay/DotpayConfigurationTest.kt new file mode 100644 index 0000000000..ac534fe929 --- /dev/null +++ b/dotpay/src/test/java/com/adyen/checkout/dotpay/DotpayConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.dotpay + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class DotpayConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + dotpay { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getDotpayConfiguration() + + val expected = DotpayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = DotpayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualDotConfig = actual.getDotpayConfiguration() + assertEquals(config.shopperLocale, actualDotConfig?.shopperLocale) + assertEquals(config.environment, actualDotConfig?.environment) + assertEquals(config.clientKey, actualDotConfig?.clientKey) + assertEquals(config.amount, actualDotConfig?.amount) + assertEquals(config.analyticsConfiguration, actualDotConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualDotConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualDotConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualDotConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/drop-in-compose/src/main/java/com/adyen/checkout/dropin/compose/ComposeExtensions.kt b/drop-in-compose/src/main/java/com/adyen/checkout/dropin/compose/ComposeExtensions.kt index 3f1fa02bc9..edf8890786 100644 --- a/drop-in-compose/src/main/java/com/adyen/checkout/dropin/compose/ComposeExtensions.kt +++ b/drop-in-compose/src/main/java/com/adyen/checkout/dropin/compose/ComposeExtensions.kt @@ -14,6 +14,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.dropin.DropIn import com.adyen.checkout.dropin.DropIn.registerForDropInResult @@ -51,7 +52,7 @@ fun rememberLauncherForDropInResult( ): ActivityResultLauncher { return rememberLauncherForActivityResult( contract = SessionDropInResultContract(), - onResult = callback::onDropInResult + onResult = callback::onDropInResult, ) } @@ -88,7 +89,45 @@ fun DropIn.startPayment( dropInLauncher = dropInLauncher, checkoutSession = checkoutSession, dropInConfiguration = dropInConfiguration, - serviceClass = serviceClass + serviceClass = serviceClass, + ) + } +} + +/** + * Starts the checkout flow to be handled by the Drop-in solution. With this solution your backend only needs to + * integrate the /sessions endpoint to start the checkout flow. + * + * Call [rememberLauncherForDropInResult] to create a launcher and receive the final result of Drop-in. + * + * Use [checkoutConfiguration] to configure Drop-in and the components that will be loaded inside it. + * + * Optionally, you can extend [SessionDropInService] with your own implementation and add it to your manifest file. + * This allows you to interact with Drop-in, and take over the checkout flow. + * + * @param dropInLauncher A launcher to start Drop-in, obtained with [registerForDropInResult]. + * @param checkoutSession The result from the /sessions endpoint passed onto [CheckoutSessionProvider.createSession] + * to create this object. + * @param checkoutConfiguration Additional required configuration data. + * @param serviceClass Service that extends from [SessionDropInService] to optionally take over the checkout flow. + */ +@SuppressLint("ComposableNaming") +@Suppress("unused") +@Composable +fun DropIn.startPayment( + dropInLauncher: ActivityResultLauncher, + checkoutSession: CheckoutSession, + checkoutConfiguration: CheckoutConfiguration = checkoutSession.getConfiguration(), + serviceClass: Class = SessionDropInService::class.java, +) { + val currentContext = LocalContext.current + LaunchedEffect(Unit) { + startPayment( + context = currentContext, + dropInLauncher = dropInLauncher, + checkoutSession = checkoutSession, + checkoutConfiguration = checkoutConfiguration, + serviceClass = serviceClass, ) } } @@ -113,7 +152,7 @@ fun rememberLauncherForDropInResult( ): ActivityResultLauncher { return rememberLauncherForActivityResult( contract = DropInResultContract(), - onResult = callback::onDropInResult + onResult = callback::onDropInResult, ) } @@ -149,7 +188,44 @@ fun DropIn.startPayment( dropInLauncher = dropInLauncher, paymentMethodsApiResponse = paymentMethodsApiResponse, dropInConfiguration = dropInConfiguration, - serviceClass = serviceClass + serviceClass = serviceClass, + ) + } +} + +/** + * Starts the advanced checkout flow to be handled by the Drop-in solution. With this solution your backend needs to + * integrate the 3 main API endpoints: /paymentMethods, /payments and /payments/details. + * + * Extend [DropInService] with your own implementation and add it to your manifest file. This class allows you to + * interact with Drop-in during the checkout flow. + * + * Call [rememberLauncherForDropInResult] to create a launcher and receive the final result of Drop-in. + * + * Use [checkoutConfiguration] to configure Drop-in and the components that will be loaded inside it. + * + * @param dropInLauncher A launcher to start Drop-in, obtained with [registerForDropInResult]. + * @param paymentMethodsApiResponse The result from the /paymentMethods endpoint. + * @param checkoutConfiguration Additional required configuration data. + * @param serviceClass Service that extends from [DropInService] to interact with Drop-in during the checkout flow. + */ +@SuppressLint("ComposableNaming") +@Suppress("unused") +@Composable +fun DropIn.startPayment( + dropInLauncher: ActivityResultLauncher, + paymentMethodsApiResponse: PaymentMethodsApiResponse, + checkoutConfiguration: CheckoutConfiguration, + serviceClass: Class, +) { + val currentContext = LocalContext.current + LaunchedEffect(Unit) { + startPayment( + context = currentContext, + dropInLauncher = dropInLauncher, + paymentMethodsApiResponse = paymentMethodsApiResponse, + checkoutConfiguration = checkoutConfiguration, + serviceClass = serviceClass, ) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt index 4783a7fa27..332df612c3 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/BaseDropInServiceContract.kt @@ -10,9 +10,11 @@ package com.adyen.checkout.dropin import android.os.Bundle import com.adyen.checkout.card.BinLookupData +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.core.exception.MethodNotImplementedException +@Suppress("TooManyFunctions") interface BaseDropInServiceContract { /** @@ -85,6 +87,17 @@ interface BaseDropInServiceContract { */ fun sendRecurringResult(result: RecurringDropInServiceResult) + /** + * Allows sending the result of Address Lookup operations. + * + * Call this method with a [AddressLookupDropInServiceResult] depending on the performed address lookup action. + * + * Check the subclasses of [AddressLookupDropInServiceResult] for more information. + * + * @param result the result of the action. + */ + fun sendAddressLookupResult(result: AddressLookupDropInServiceResult) + /** * Gets the additional data that was set when starting Drop-in using * [DropInConfiguration.Builder.setAdditionalDataForDropInService] or null if nothing was set. @@ -109,4 +122,21 @@ interface BaseDropInServiceContract { * @param data A list of [BinLookupData], which contains information about the detected brands. */ fun onBinLookup(data: List) = Unit + + /** + * Set a callback that will be called when shopper inputs a query to perform address lookup. + * + * @param query Query inputted by shopper. + */ + fun onAddressLookupQueryChanged(query: String) { + throw MethodNotImplementedException("Method onAddressLookupQueryChanged is not implemented") + } + + /** + * Set a callback that will be called when shopper chooses an address option that requires complete details to be + * provided. + * + * @param lookupAddress Address option selected by shopper. + */ + fun onAddressLookupCompletion(lookupAddress: LookupAddress) = false } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt index a09b4b4aae..3325718186 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropIn.kt @@ -11,17 +11,22 @@ package com.adyen.checkout.dropin import android.content.Context import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodsApiResponse +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.internal.util.BuildUtils -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.DropIn.registerForDropInResult import com.adyen.checkout.dropin.DropIn.startPayment +import com.adyen.checkout.dropin.internal.ui.model.DropInParamsMapper import com.adyen.checkout.dropin.internal.ui.model.DropInResultContractParams import com.adyen.checkout.dropin.internal.ui.model.SessionDropInResultContractParams import com.adyen.checkout.dropin.internal.util.DropInPrefs import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.CheckoutSessionProvider +import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory +import java.util.Locale /** * Drop-in is our pre-built checkout UI for accepting payments. You only need to integrate through your backend with the @@ -34,7 +39,6 @@ import com.adyen.checkout.sessions.core.CheckoutSessionProvider * of Drop-in. Then call one of the [startPayment] methods. */ object DropIn { - private val TAG = LogUtil.getTag() internal const val RESULT_KEY = "payment_result" internal const val SESSION_RESULT_KEY = "session_payment_result" @@ -92,13 +96,52 @@ object DropIn { dropInConfiguration: DropInConfiguration, serviceClass: Class = SessionDropInService::class.java, ) { - Logger.d(TAG, "startPayment with sessions") + startPayment( + context = context, + dropInLauncher = dropInLauncher, + checkoutSession = checkoutSession, + checkoutConfiguration = dropInConfiguration.toCheckoutConfiguration(), + serviceClass = serviceClass, + ) + } + + /** + * Starts the checkout flow to be handled by the Drop-in solution. With this solution your backend only needs to + * integrate the /sessions endpoint to start the checkout flow. + * + * Call [registerForDropInResult] to create a launcher when initializing your Activity or Fragment and receive the + * final result of Drop-in. + * + * Use [checkoutConfiguration] to configure Drop-in and the components that will be loaded inside it. + * + * Optionally, you can extend [SessionDropInService] with your own implementation and add it to your manifest file. + * This allows you to interact with Drop-in, and take over the checkout flow. + * + * @param context The context to start the Checkout flow with. + * @param dropInLauncher A launcher to start Drop-in, obtained with [registerForDropInResult]. + * @param checkoutSession The result from the /sessions endpoint passed onto [CheckoutSessionProvider.createSession] + * to create this object. + * @param checkoutConfiguration Additional required configuration data. + * @param serviceClass Service that extends from [SessionDropInService] to optionally take over the checkout flow. + */ + @JvmStatic + fun startPayment( + context: Context, + dropInLauncher: ActivityResultLauncher, + checkoutSession: CheckoutSession, + checkoutConfiguration: CheckoutConfiguration = checkoutSession.getConfiguration(), + serviceClass: Class = SessionDropInService::class.java, + ) { + adyenLog(AdyenLogLevel.DEBUG) { "startPayment with sessions" } val sessionDropInResultContractParams = SessionDropInResultContractParams( - dropInConfiguration, + checkoutConfiguration, checkoutSession, serviceClass, ) - startPayment(context, dropInLauncher, dropInConfiguration, sessionDropInResultContractParams) + val sessionParams = SessionParamsFactory.create(checkoutSession) + val shopperLocale = DropInParamsMapper().getShopperLocale(checkoutConfiguration, sessionParams) + + startPayment(context, dropInLauncher, shopperLocale, sessionDropInResultContractParams) } /** @@ -150,27 +193,69 @@ object DropIn { dropInConfiguration: DropInConfiguration, serviceClass: Class, ) { - Logger.d(TAG, "startPayment with payment methods") + startPayment( + context = context, + dropInLauncher = dropInLauncher, + paymentMethodsApiResponse = paymentMethodsApiResponse, + checkoutConfiguration = dropInConfiguration.toCheckoutConfiguration(), + serviceClass = serviceClass, + ) + } + + /** + * Starts the advanced checkout flow to be handled by the Drop-in solution. With this solution your backend needs to + * integrate the 3 main API endpoints: /paymentMethods, /payments and /payments/details. + * + * Extend [DropInService] with your own implementation and add it to your manifest file. This class allows you to + * interact with Drop-in during the checkout flow. + * + * Call [registerForDropInResult] to create a launcher when initializing your Activity or Fragment and receive the + * final result of Drop-in. + * + * Use [checkoutConfiguration] to configure Drop-in and the components that will be loaded inside it. + * + * @param context The context to start the Checkout flow with. + * @param dropInLauncher A launcher to start Drop-in, obtained with [registerForDropInResult]. + * @param paymentMethodsApiResponse The result from the /paymentMethods endpoint. + * @param checkoutConfiguration Additional required configuration data. + * @param serviceClass Service that extends from [DropInService] to interact with Drop-in during the checkout flow. + */ + @JvmStatic + fun startPayment( + context: Context, + dropInLauncher: ActivityResultLauncher, + paymentMethodsApiResponse: PaymentMethodsApiResponse, + checkoutConfiguration: CheckoutConfiguration, + serviceClass: Class, + ) { + adyenLog(AdyenLogLevel.DEBUG) { "startPayment with payment methods" } val dropInResultContractParams = DropInResultContractParams( - dropInConfiguration, + checkoutConfiguration, paymentMethodsApiResponse, serviceClass, ) - startPayment(context, dropInLauncher, dropInConfiguration, dropInResultContractParams) + val shopperLocale = DropInParamsMapper().getShopperLocale(checkoutConfiguration, null) + + startPayment(context, dropInLauncher, shopperLocale, dropInResultContractParams) } private fun startPayment( context: Context, dropInLauncher: ActivityResultLauncher, - dropInConfiguration: DropInConfiguration, + shopperLocale: Locale?, params: T, ) { updateDefaultLogLevel(context) - DropInPrefs.setShopperLocale(context, dropInConfiguration.shopperLocale) + DropInPrefs.setShopperLocale(context, shopperLocale) dropInLauncher.launch(params) } private fun updateDefaultLogLevel(context: Context) { - Logger.updateDefaultLogLevel(BuildUtils.isDebugBuild(context)) + val logLevel = if (BuildUtils.isDebugBuild(context)) { + AdyenLogLevel.DEBUG + } else { + AdyenLogLevel.NONE + } + AdyenLogger.setLogLevel(logLevel) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt index 677c9c85b3..2645bbd554 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInConfiguration.kt @@ -21,8 +21,10 @@ import com.adyen.checkout.card.CardConfiguration import com.adyen.checkout.cashapppay.CashAppPayConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPConfiguration import com.adyen.checkout.core.Environment import com.adyen.checkout.dotpay.DotpayConfiguration @@ -57,26 +59,38 @@ import kotlin.collections.set @Parcelize @Suppress("LongParameterList") class DropInConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, override val amount: Amount?, private val availablePaymentConfigs: HashMap, internal val genericActionConfiguration: GenericActionConfiguration, - val showPreselectedStoredPaymentMethod: Boolean, - val skipListWhenSinglePaymentMethod: Boolean, - val isRemovingStoredPaymentMethodsEnabled: Boolean, + val showPreselectedStoredPaymentMethod: Boolean?, + val skipListWhenSinglePaymentMethod: Boolean?, + val isRemovingStoredPaymentMethodsEnabled: Boolean?, val additionalDataForDropInService: Bundle?, internal val overriddenPaymentMethodInformation: HashMap, ) : Configuration { - internal fun getConfigurationForPaymentMethod(paymentMethod: String): T? { - if (availablePaymentConfigs.containsKey(paymentMethod)) { - @Suppress("UNCHECKED_CAST") - return availablePaymentConfigs[paymentMethod] as T + internal fun toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(DROP_IN_CONFIG_KEY, this@DropInConfiguration) + + availablePaymentConfigs.forEach { (key, paymentConfig) -> + addConfiguration(key, paymentConfig) + } + + genericActionConfiguration.getAllConfigurations().forEach { config -> + addActionConfiguration(config) + } } - return null } /** @@ -89,11 +103,27 @@ class DropInConfiguration private constructor( private val availablePaymentConfigs = HashMap() private val overriddenPaymentMethodInformation = HashMap() - private var showPreselectedStoredPaymentMethod: Boolean = true - private var skipListWhenSinglePaymentMethod: Boolean = false - private var isRemovingStoredPaymentMethodsEnabled: Boolean = false + private var showPreselectedStoredPaymentMethod: Boolean? = null + private var skipListWhenSinglePaymentMethod: Boolean? = null + private var isRemovingStoredPaymentMethodsEnabled: Boolean? = null private var additionalDataForDropInService: Bundle? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Create a [DropInConfiguration] * @@ -104,7 +134,7 @@ class DropInConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -114,14 +144,17 @@ class DropInConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** * When set to false, Drop-in will skip the preselected screen and go straight to the payment methods list. + * + * Default is true. */ fun setShowPreselectedStoredPaymentMethod(showStoredPaymentMethod: Boolean): Builder { this.showPreselectedStoredPaymentMethod = showStoredPaymentMethod @@ -135,6 +168,8 @@ class DropInConfiguration private constructor( * This only applies to payment methods that require a component (user input). Which means redirect payment * methods, SDK payment methods, etc will not be skipped even if this flag is set to true and a single payment * method is present. + * + * Default is false. */ fun setSkipListWhenSinglePaymentMethod(skipListWhenSinglePaymentMethod: Boolean): Builder { this.skipListWhenSinglePaymentMethod = skipListWhenSinglePaymentMethod @@ -146,6 +181,8 @@ class DropInConfiguration private constructor( * the payment methods screen. * * You need to implement [DropInService.onRemoveStoredPaymentMethod] to handle the removal. + * + * Default is false. */ fun setEnableRemovingStoredPaymentMethods(isEnabled: Boolean): Builder { this.isRemovingStoredPaymentMethodsEnabled = isEnabled @@ -420,3 +457,24 @@ class DropInConfiguration private constructor( } } } + +private const val DROP_IN_CONFIG_KEY = "DROP_IN_CONFIG_KEY" + +fun CheckoutConfiguration.dropIn( + configuration: @CheckoutConfigurationMarker Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(DROP_IN_CONFIG_KEY, config) + return this +} + +fun CheckoutConfiguration.getDropInConfiguration(): DropInConfiguration? { + return getConfiguration(DROP_IN_CONFIG_KEY) +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt index 297f7bed8e..6163f1655c 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInResultContract.kt @@ -20,7 +20,7 @@ class DropInResultContract : ActivityResultContract) { - Logger.d(TAG, "requestPaymentsCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestPaymentsCall" } onSubmit(paymentComponentState) } final override fun requestDetailsCall(actionComponentData: ActionComponentData) { - Logger.d(TAG, "requestDetailsCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestDetailsCall" } onAdditionalDetails(actionComponentData) } final override fun requestBalanceCall(paymentComponentState: PaymentComponentState<*>) { - Logger.d(TAG, "requestBalanceCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestBalanceCall" } onBalanceCheck(paymentComponentState) } final override fun requestOrdersCall() { - Logger.d(TAG, "requestOrdersCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestOrdersCall" } onOrderRequest() } final override fun requestCancelOrder(order: OrderRequest, isDropInCancelledByUser: Boolean) { - Logger.d(TAG, "requestCancelOrder") + adyenLog(AdyenLogLevel.DEBUG) { "requestCancelOrder" } onOrderCancel(order, !isDropInCancelledByUser) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInServiceResult.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInServiceResult.kt index 0d708faa89..a388826ee3 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/DropInServiceResult.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/DropInServiceResult.kt @@ -9,6 +9,7 @@ package com.adyen.checkout.dropin import com.adyen.checkout.components.core.BalanceResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.sessions.core.SessionPaymentResult @@ -185,6 +186,48 @@ sealed class RecurringDropInServiceResult : BaseDropInServiceResult() { ) : RecurringDropInServiceResult(), DropInServiceResultError } +sealed class AddressLookupDropInServiceResult : BaseDropInServiceResult() { + + /** + * Only applicable to address lookup flow. + * + * Send this to display the options received for the query shopper has inputted. + * + * @param options Address options to be displayed to the shopper. + */ + class LookupResult( + val options: List + ) : AddressLookupDropInServiceResult() + + /** + * Only applicable to address lookup flow. + * + * Send this to prefill the address after making an api call to fetch the complete address details. + * + * @param lookupAddress Complete address details. + */ + class LookupComplete( + val lookupAddress: LookupAddress + ) : AddressLookupDropInServiceResult() + + /** + * * Only applicable to address lookup flow. + * + * Send this to display an error dialog and optionally dismiss Drop-in. + * + * @param errorDialog If set, a dialog will be shown with the data passed in [ErrorDialog]. If null, no + * dialog will be displayed. + * @param reason the reason of the error. You will receive this value back in your [DropInCallback] class. This + * value is not used internally by Drop-in. + * @param dismissDropIn whether Drop-in should be dismissed after presenting the Alert Dialog. + */ + class Error( + override val errorDialog: ErrorDialog?, + override val reason: String? = null, + override val dismissDropIn: Boolean = false, + ) : AddressLookupDropInServiceResult(), DropInServiceResultError +} + internal sealed class SessionDropInServiceResult : BaseDropInServiceResult() { data class SessionDataChanged(val sessionData: String) : SessionDropInServiceResult() diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResult.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResult.kt index 01283d4e8f..055eb55207 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResult.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResult.kt @@ -36,7 +36,7 @@ sealed class SessionDropInResult { * Drop-in has completed. * This occurs after the payment is finished. * - * @param result The result of Drop-in. + * @param result The result of the payment. */ class Finished(val result: SessionPaymentResult) : SessionDropInResult() } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResultContract.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResultContract.kt index f18a05ebed..51abd4577f 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResultContract.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInResultContract.kt @@ -22,7 +22,7 @@ class SessionDropInResultContract : override fun createIntent(context: Context, input: SessionDropInResultContractParams): Intent { return DropInActivity.createIntent( context = context, - dropInConfiguration = input.dropInConfiguration, + checkoutConfiguration = input.checkoutConfiguration, checkoutSession = input.checkoutSession, service = ComponentName(context, input.serviceClass), ) @@ -43,9 +43,11 @@ class SessionDropInResultContract : SessionDropInResult.Error(reason) } } + resultCode == Activity.RESULT_OK && data.hasExtra(DropIn.SESSION_RESULT_KEY) -> { SessionDropInResult.Finished(requireNotNull(data.getParcelableExtra(DropIn.SESSION_RESULT_KEY))) } + resultCode == Activity.RESULT_OK && data.hasExtra(DropIn.RESULT_KEY) -> { val result = data.getStringExtra(DropIn.RESULT_KEY) SessionDropInResult.Finished( @@ -55,9 +57,10 @@ class SessionDropInResultContract : sessionData = null, resultCode = result, order = null, - ) + ), ) } + else -> null } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt index d6ea5b381f..a3e438a23d 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt @@ -12,10 +12,11 @@ import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.PaymentComponentState +import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.Environment import com.adyen.checkout.core.internal.data.api.HttpClientFactory -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.internal.service.BaseDropInService import com.adyen.checkout.dropin.internal.service.SessionDropInServiceInterface import com.adyen.checkout.sessions.core.SessionModel @@ -71,7 +72,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter } private fun sendSessionDataChangedResult(sessionData: String) { - Logger.d(TAG, "Sending session data changed result - $sessionData") + adyenLog(AdyenLogLevel.DEBUG) { "Sending session data changed result - $sessionData" } val result = SessionDropInServiceResult.SessionDataChanged(sessionData) emitResult(result) } @@ -88,7 +89,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter is SessionCallResult.Payments.Action -> DropInServiceResult.Action(result.action) is SessionCallResult.Payments.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.payment_failed)), reason = result.throwable.message, dismissDropIn = true, ) @@ -97,7 +98,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter is SessionCallResult.Payments.NotFullyPaidOrder -> updatePaymentMethods(result.result.order) is SessionCallResult.Payments.RefusedPartialPayment -> DropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.payment_failed)), reason = "Payment was refused while making a partial payment", ) @@ -123,7 +124,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter is SessionCallResult.Details.Action -> DropInServiceResult.Action(result.action) is SessionCallResult.Details.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.payment_failed)), reason = result.throwable.message, dismissDropIn = true, ) @@ -150,7 +151,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter val dropInServiceResult = when (result) { is SessionCallResult.Balance.Error -> BalanceDropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.payment_failed)), reason = result.throwable.message, ) @@ -175,7 +176,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter val dropInServiceResult = when (result) { is SessionCallResult.CreateOrder.Error -> OrderDropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.payment_failed)), reason = result.throwable.message, dismissDropIn = true, ) @@ -203,7 +204,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter val dropInServiceResult = when (result) { is SessionCallResult.CancelOrder.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.unknown_error)), reason = result.throwable.message, ) @@ -222,6 +223,27 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter } } + override fun onRemoveStoredPaymentMethod(storedPaymentMethod: StoredPaymentMethod) { + launch { + val storedPaymentMethodId = storedPaymentMethod.id.orEmpty() + val result = sessionInteractor.removeStoredPaymentMethod(storedPaymentMethodId) + + val serviceResult = when (result) { + SessionCallResult.RemoveStoredPaymentMethod.Successful -> + RecurringDropInServiceResult.PaymentMethodRemoved( + storedPaymentMethodId, + ) + + is SessionCallResult.RemoveStoredPaymentMethod.Error -> RecurringDropInServiceResult.Error( + errorDialog = ErrorDialog(message = getString(R.string.unknown_error)), + reason = result.throwable.message, + ) + } + + sendRecurringResult(serviceResult) + } + } + private suspend fun updatePaymentMethods(order: OrderResponse? = null): DropInServiceResult { return when (val result = sessionInteractor.updatePaymentMethods(order)) { is SessionCallResult.UpdatePaymentMethods.Successful -> DropInServiceResult.Update( @@ -231,7 +253,7 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter is SessionCallResult.UpdatePaymentMethods.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(), + errorDialog = ErrorDialog(message = getString(R.string.payment_failed)), reason = result.throwable.message, dismissDropIn = true, ) @@ -241,12 +263,8 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter private fun sendFlowTakenOverUpdatedResult() { if (isFlowTakenOver) return isFlowTakenOver = true - Logger.i(TAG, "Flow was taken over, sending update to drop-in") + adyenLog(AdyenLogLevel.INFO) { "Flow was taken over, sending update to drop-in" } val result = SessionDropInServiceResult.SessionTakenOverUpdated(true) emitResult(result) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/ComponentParsingProvider.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/ComponentParsingProvider.kt deleted file mode 100644 index 841f078ad4..0000000000 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/ComponentParsingProvider.kt +++ /dev/null @@ -1,948 +0,0 @@ -/* - * Copyright (c) 2019 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by caiof on 24/4/2019. - */ - -@file:Suppress("TooManyFunctions") - -package com.adyen.checkout.dropin.internal.provider - -import android.app.Application -import android.content.Context -import androidx.fragment.app.Fragment -import com.adyen.checkout.ach.ACHDirectDebitComponent -import com.adyen.checkout.ach.ACHDirectDebitComponentState -import com.adyen.checkout.ach.ACHDirectDebitConfiguration -import com.adyen.checkout.ach.internal.provider.ACHDirectDebitComponentProvider -import com.adyen.checkout.bacs.BacsDirectDebitComponent -import com.adyen.checkout.bacs.BacsDirectDebitComponentState -import com.adyen.checkout.bacs.BacsDirectDebitConfiguration -import com.adyen.checkout.bacs.internal.provider.BacsDirectDebitComponentProvider -import com.adyen.checkout.bcmc.BcmcComponent -import com.adyen.checkout.bcmc.BcmcComponentState -import com.adyen.checkout.bcmc.BcmcConfiguration -import com.adyen.checkout.bcmc.internal.provider.BcmcComponentProvider -import com.adyen.checkout.blik.BlikComponent -import com.adyen.checkout.blik.BlikComponentState -import com.adyen.checkout.blik.BlikConfiguration -import com.adyen.checkout.blik.internal.provider.BlikComponentProvider -import com.adyen.checkout.boleto.BoletoComponent -import com.adyen.checkout.boleto.BoletoComponentState -import com.adyen.checkout.boleto.BoletoConfiguration -import com.adyen.checkout.boleto.internal.provider.BoletoComponentProvider -import com.adyen.checkout.card.CardComponent -import com.adyen.checkout.card.CardComponentState -import com.adyen.checkout.card.CardConfiguration -import com.adyen.checkout.card.internal.provider.CardComponentProvider -import com.adyen.checkout.cashapppay.CashAppPayComponent -import com.adyen.checkout.cashapppay.CashAppPayComponentState -import com.adyen.checkout.cashapppay.CashAppPayConfiguration -import com.adyen.checkout.cashapppay.internal.provider.CashAppPayComponentProvider -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.ComponentAvailableCallback -import com.adyen.checkout.components.core.ComponentCallback -import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.PaymentMethodTypes -import com.adyen.checkout.components.core.StoredPaymentMethod -import com.adyen.checkout.components.core.internal.AlwaysAvailablePaymentMethod -import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder -import com.adyen.checkout.components.core.internal.Configuration -import com.adyen.checkout.components.core.internal.NotAvailablePaymentMethod -import com.adyen.checkout.components.core.internal.PaymentComponent -import com.adyen.checkout.components.core.internal.PaymentMethodAvailabilityCheck -import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPComponent -import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPComponentState -import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPConfiguration -import com.adyen.checkout.conveniencestoresjp.internal.provider.ConvenienceStoresJPComponentProvider -import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.core.internal.util.runCompileOnly -import com.adyen.checkout.dotpay.DotpayComponent -import com.adyen.checkout.dotpay.DotpayComponentState -import com.adyen.checkout.dotpay.DotpayConfiguration -import com.adyen.checkout.dotpay.internal.provider.DotpayComponentProvider -import com.adyen.checkout.dropin.DropInConfiguration -import com.adyen.checkout.dropin.internal.ui.BacsDirectDebitDialogFragment -import com.adyen.checkout.dropin.internal.ui.CardComponentDialogFragment -import com.adyen.checkout.dropin.internal.ui.DropInBottomSheetDialogFragment -import com.adyen.checkout.dropin.internal.ui.GenericComponentDialogFragment -import com.adyen.checkout.dropin.internal.ui.GiftCardComponentDialogFragment -import com.adyen.checkout.dropin.internal.ui.GooglePayComponentDialogFragment -import com.adyen.checkout.dropin.internal.ui.model.DropInComponentParams -import com.adyen.checkout.dropin.internal.ui.model.DropInComponentParamsMapper -import com.adyen.checkout.entercash.EntercashComponent -import com.adyen.checkout.entercash.EntercashComponentState -import com.adyen.checkout.entercash.EntercashConfiguration -import com.adyen.checkout.entercash.internal.provider.EntercashComponentProvider -import com.adyen.checkout.eps.EPSComponent -import com.adyen.checkout.eps.EPSComponentState -import com.adyen.checkout.eps.EPSConfiguration -import com.adyen.checkout.eps.internal.provider.EPSComponentProvider -import com.adyen.checkout.giftcard.GiftCardComponent -import com.adyen.checkout.giftcard.GiftCardComponentCallback -import com.adyen.checkout.giftcard.GiftCardConfiguration -import com.adyen.checkout.giftcard.internal.provider.GiftCardComponentProvider -import com.adyen.checkout.googlepay.GooglePayComponent -import com.adyen.checkout.googlepay.GooglePayComponentState -import com.adyen.checkout.googlepay.GooglePayConfiguration -import com.adyen.checkout.googlepay.internal.provider.GooglePayComponentProvider -import com.adyen.checkout.ideal.IdealComponent -import com.adyen.checkout.ideal.IdealComponentState -import com.adyen.checkout.ideal.IdealConfiguration -import com.adyen.checkout.ideal.internal.provider.IdealComponentProvider -import com.adyen.checkout.instant.InstantComponentState -import com.adyen.checkout.instant.InstantPaymentComponent -import com.adyen.checkout.instant.InstantPaymentConfiguration -import com.adyen.checkout.instant.internal.provider.InstantPaymentComponentProvider -import com.adyen.checkout.mbway.MBWayComponent -import com.adyen.checkout.mbway.MBWayComponentState -import com.adyen.checkout.mbway.MBWayConfiguration -import com.adyen.checkout.mbway.internal.provider.MBWayComponentProvider -import com.adyen.checkout.molpay.MolpayComponent -import com.adyen.checkout.molpay.MolpayComponentState -import com.adyen.checkout.molpay.MolpayConfiguration -import com.adyen.checkout.molpay.internal.provider.MolpayComponentProvider -import com.adyen.checkout.onlinebankingcz.OnlineBankingCZComponent -import com.adyen.checkout.onlinebankingcz.OnlineBankingCZComponentState -import com.adyen.checkout.onlinebankingcz.OnlineBankingCZConfiguration -import com.adyen.checkout.onlinebankingcz.internal.provider.OnlineBankingCZComponentProvider -import com.adyen.checkout.onlinebankingjp.OnlineBankingJPComponent -import com.adyen.checkout.onlinebankingjp.OnlineBankingJPComponentState -import com.adyen.checkout.onlinebankingjp.OnlineBankingJPConfiguration -import com.adyen.checkout.onlinebankingjp.internal.provider.OnlineBankingJPComponentProvider -import com.adyen.checkout.onlinebankingpl.OnlineBankingPLComponent -import com.adyen.checkout.onlinebankingpl.OnlineBankingPLComponentState -import com.adyen.checkout.onlinebankingpl.OnlineBankingPLConfiguration -import com.adyen.checkout.onlinebankingpl.internal.provider.OnlineBankingPLComponentProvider -import com.adyen.checkout.onlinebankingsk.OnlineBankingSKComponent -import com.adyen.checkout.onlinebankingsk.OnlineBankingSKComponentState -import com.adyen.checkout.onlinebankingsk.OnlineBankingSKConfiguration -import com.adyen.checkout.onlinebankingsk.internal.provider.OnlineBankingSKComponentProvider -import com.adyen.checkout.openbanking.OpenBankingComponent -import com.adyen.checkout.openbanking.OpenBankingComponentState -import com.adyen.checkout.openbanking.OpenBankingConfiguration -import com.adyen.checkout.openbanking.internal.provider.OpenBankingComponentProvider -import com.adyen.checkout.paybybank.PayByBankComponent -import com.adyen.checkout.paybybank.PayByBankComponentState -import com.adyen.checkout.paybybank.PayByBankConfiguration -import com.adyen.checkout.paybybank.internal.provider.PayByBankComponentProvider -import com.adyen.checkout.payeasy.PayEasyComponent -import com.adyen.checkout.payeasy.PayEasyComponentState -import com.adyen.checkout.payeasy.PayEasyConfiguration -import com.adyen.checkout.payeasy.internal.provider.PayEasyComponentProvider -import com.adyen.checkout.sepa.SepaComponent -import com.adyen.checkout.sepa.SepaComponentState -import com.adyen.checkout.sepa.SepaConfiguration -import com.adyen.checkout.sepa.internal.provider.SepaComponentProvider -import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails -import com.adyen.checkout.sessions.core.internal.data.model.mapToParams -import com.adyen.checkout.seveneleven.SevenElevenComponent -import com.adyen.checkout.seveneleven.SevenElevenComponentState -import com.adyen.checkout.seveneleven.SevenElevenConfiguration -import com.adyen.checkout.seveneleven.internal.provider.SevenElevenComponentProvider -import com.adyen.checkout.upi.UPIComponent -import com.adyen.checkout.upi.UPIComponentState -import com.adyen.checkout.upi.UPIConfiguration -import com.adyen.checkout.upi.internal.provider.UPIComponentProvider -import com.adyen.checkout.wechatpay.WeChatPayProvider - -private val TAG = LogUtil.getTag() - -internal inline fun getConfigurationForPaymentMethod( - paymentMethod: PaymentMethod, - dropInConfiguration: DropInConfiguration, - context: Context, -): T { - val paymentMethodType = paymentMethod.type ?: throw CheckoutException("Payment method type is null") - return dropInConfiguration.getConfigurationForPaymentMethod(paymentMethodType) ?: getDefaultConfigForPaymentMethod( - paymentMethod, - dropInConfiguration, - context, - ) -} - -internal inline fun getConfigurationForPaymentMethod( - storedPaymentMethod: StoredPaymentMethod, - dropInConfiguration: DropInConfiguration, -): T { - val storedPaymentMethodType = storedPaymentMethod.type ?: throw CheckoutException("Payment method type is null") - return dropInConfiguration.getConfigurationForPaymentMethod(storedPaymentMethodType) - ?: getDefaultConfigForPaymentMethod( - storedPaymentMethod = storedPaymentMethod, - dropInConfiguration = dropInConfiguration - ) -} - -internal fun getDefaultConfigForPaymentMethod( - storedPaymentMethod: StoredPaymentMethod, - dropInConfiguration: DropInConfiguration -): T { - val shopperLocale = dropInConfiguration.shopperLocale - val environment = dropInConfiguration.environment - val clientKey = dropInConfiguration.clientKey - val builder: BaseConfigurationBuilder<*, *> = when { - checkCompileOnly { ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> - ACHDirectDebitConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { BlikComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> - BlikConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> - CardConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { CashAppPayComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> - CashAppPayConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - else -> throw CheckoutException( - errorMessage = "Unable to find component configuration for storedPaymentMethod - $storedPaymentMethod" - ) - } - @Suppress("UNCHECKED_CAST") - return builder.build() as T -} - -@Suppress("LongMethod", "CyclomaticComplexMethod") -internal fun getDefaultConfigForPaymentMethod( - paymentMethod: PaymentMethod, - dropInConfiguration: DropInConfiguration, - context: Context, -): T { - val shopperLocale = dropInConfiguration.shopperLocale - val environment = dropInConfiguration.environment - val clientKey = dropInConfiguration.clientKey - - // get default builder for Configuration type - val builder: BaseConfigurationBuilder<*, *> = when { - checkCompileOnly { ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - ACHDirectDebitConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - BacsDirectDebitConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { BcmcComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - BcmcConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { BlikComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - BlikConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { BoletoComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - BoletoConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - CardConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { CashAppPayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - CashAppPayConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - .setReturnUrl(CashAppPayComponent.getReturnUrl(context)) - - checkCompileOnly { ConvenienceStoresJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - ConvenienceStoresJPConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { DotpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - DotpayConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { EntercashComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - EntercashConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { EPSComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - EPSConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - GiftCardConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - GooglePayConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { IdealComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - IdealConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { InstantPaymentComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - InstantPaymentConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { MBWayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - MBWayConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { MolpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - MolpayConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { OnlineBankingCZComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - OnlineBankingCZConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { OnlineBankingJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - OnlineBankingJPConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { OnlineBankingPLComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - OnlineBankingPLConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { OnlineBankingSKComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - OnlineBankingSKConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { OpenBankingComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - OpenBankingConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { PayByBankComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - PayByBankConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { PayEasyComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - PayEasyConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { SepaComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - SepaConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { SevenElevenComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - SevenElevenConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey - ) - - checkCompileOnly { UPIComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> - UPIConfiguration.Builder( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - ) - - else -> throw CheckoutException("Unable to find component configuration for paymentMethod - $paymentMethod") - } - - @Suppress("UNCHECKED_CAST") - return builder.build() as T -} - -private inline fun getConfigurationForPaymentMethodOrNull( - paymentMethod: PaymentMethod, - dropInConfiguration: DropInConfiguration, - context: Context, -): T? { - @Suppress("SwallowedException") - return try { - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - } catch (e: CheckoutException) { - null - } -} - -@Suppress("LongParameterList") -internal fun checkPaymentMethodAvailability( - application: Application, - paymentMethod: PaymentMethod, - dropInConfiguration: DropInConfiguration, - amount: Amount?, - sessionDetails: SessionDetails?, - callback: ComponentAvailableCallback, -) { - try { - Logger.v(TAG, "Checking availability for type - ${paymentMethod.type}") - - val type = paymentMethod.type ?: throw CheckoutException("PaymentMethod type is null") - - val availabilityCheck = getPaymentMethodAvailabilityCheck(dropInConfiguration, type, amount, sessionDetails) - val configuration = - getConfigurationForPaymentMethodOrNull(paymentMethod, dropInConfiguration, application) - - availabilityCheck.isAvailable(application, paymentMethod, configuration, callback) - } catch (e: CheckoutException) { - Logger.e(TAG, "Unable to initiate ${paymentMethod.type}", e) - callback.onAvailabilityResult(false, paymentMethod) - } -} - -/** - * Provides the [PaymentMethodAvailabilityCheck] class for the specified [paymentMethodType], if available. - */ -internal fun getPaymentMethodAvailabilityCheck( - dropInConfiguration: DropInConfiguration, - paymentMethodType: String, - amount: Amount?, - sessionDetails: SessionDetails?, -): PaymentMethodAvailabilityCheck { - val dropInParams = dropInConfiguration.mapToParams(amount) - val sessionParams = sessionDetails?.mapToParams(amount) - - @Suppress("UNCHECKED_CAST") - val availabilityCheck = when (paymentMethodType) { - PaymentMethodTypes.GOOGLE_PAY, - PaymentMethodTypes.GOOGLE_PAY_LEGACY -> runCompileOnly { - GooglePayComponentProvider(dropInParams, sessionParams) - } - - PaymentMethodTypes.WECHAT_PAY_SDK -> runCompileOnly { WeChatPayProvider() } - else -> AlwaysAvailablePaymentMethod() - } as? PaymentMethodAvailabilityCheck - - return availabilityCheck ?: NotAvailablePaymentMethod() -} - -/** - * Provides a [PaymentComponent] from a [PaymentComponentProvider] using the [StoredPaymentMethod] reference. - * - * @param fragment The Fragment which the PaymentComponent lifecycle will be bound to. - * @param storedPaymentMethod The stored payment method to be parsed. - * @throws CheckoutException In case a component cannot be created. - */ -@Suppress("UNCHECKED_CAST", "LongParameterList") -internal fun getComponentFor( - fragment: Fragment, - storedPaymentMethod: StoredPaymentMethod, - dropInConfiguration: DropInConfiguration, - amount: Amount?, - componentCallback: ComponentCallback<*>, - sessionDetails: SessionDetails?, - analyticsRepository: AnalyticsRepository, - onRedirect: () -> Unit, -): PaymentComponent { - val dropInParams = dropInConfiguration.mapToParams(amount) - val sessionParams = sessionDetails?.mapToParams(amount) - return when { - checkCompileOnly { ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { - val achConfig: ACHDirectDebitConfiguration = - getConfigurationForPaymentMethod(storedPaymentMethod, dropInConfiguration) - ACHDirectDebitComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - storedPaymentMethod = storedPaymentMethod, - configuration = achConfig, - callback = componentCallback as ComponentCallback, - key = storedPaymentMethod.id, - ) - } - - checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { - val cardConfig: CardConfiguration = - getConfigurationForPaymentMethod(storedPaymentMethod, dropInConfiguration) - CardComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - storedPaymentMethod = storedPaymentMethod, - configuration = cardConfig, - callback = componentCallback as ComponentCallback, - key = storedPaymentMethod.id - ) - } - - checkCompileOnly { CashAppPayComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { - val cashAppPayConfig: CashAppPayConfiguration = - getConfigurationForPaymentMethod(storedPaymentMethod, dropInConfiguration) - CashAppPayComponentProvider(dropInParams, sessionParams).get( - fragment = fragment, - storedPaymentMethod = storedPaymentMethod, - configuration = cashAppPayConfig, - callback = componentCallback as ComponentCallback, - key = storedPaymentMethod.id - ) - } - - checkCompileOnly { BlikComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { - val blikConfig: BlikConfiguration = - getConfigurationForPaymentMethod(storedPaymentMethod, dropInConfiguration) - BlikComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - storedPaymentMethod = storedPaymentMethod, - configuration = blikConfig, - callback = componentCallback as ComponentCallback, - key = storedPaymentMethod.id - ) - } - - else -> { - throw CheckoutException("Unable to find stored component for type - ${storedPaymentMethod.type}") - } - }.apply { - setOnRedirectListener(onRedirect) - } -} - -/** - * Provides a [PaymentComponent] from a [PaymentComponentProvider] using the [PaymentMethod] reference. - * - * @param fragment The Fragment which the PaymentComponent lifecycle will be bound to. - * @param paymentMethod The payment method to be parsed. - * @throws CheckoutException In case a component cannot be created. - */ -@Suppress("LongMethod", "UNCHECKED_CAST", "LongParameterList", "CyclomaticComplexMethod") -internal fun getComponentFor( - fragment: Fragment, - paymentMethod: PaymentMethod, - dropInConfiguration: DropInConfiguration, - amount: Amount?, - componentCallback: ComponentCallback<*>, - sessionDetails: SessionDetails?, - analyticsRepository: AnalyticsRepository, - onRedirect: () -> Unit, -): PaymentComponent { - val dropInParams = dropInConfiguration.mapToParams(amount) - val sessionParams = sessionDetails?.mapToParams(amount) - val context = fragment.requireContext() - return when { - checkCompileOnly { ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val configuration: ACHDirectDebitConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - ACHDirectDebitComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = configuration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val bacsConfiguration: BacsDirectDebitConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - BacsDirectDebitComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = bacsConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { BcmcComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val bcmcConfiguration: BcmcConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - BcmcComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = bcmcConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { BlikComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val blikConfiguration: BlikConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - BlikComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = blikConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { BoletoComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val boletoConfiguration: BoletoConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - BoletoComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = boletoConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val cardConfig: CardConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - CardComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = cardConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { CashAppPayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val cashAppPayConfiguration: CashAppPayConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - CashAppPayComponentProvider(dropInParams, sessionParams).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = cashAppPayConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { ConvenienceStoresJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val convenienceStoresJPConfiguration: ConvenienceStoresJPConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - ConvenienceStoresJPComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = convenienceStoresJPConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { DotpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val dotpayConfig: DotpayConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - DotpayComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = dotpayConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { EntercashComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val entercashConfig: EntercashConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - EntercashComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = entercashConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { EPSComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val epsConfig: EPSConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - EPSComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = epsConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val giftcardConfiguration: GiftCardConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - GiftCardComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = giftcardConfiguration, - callback = componentCallback as GiftCardComponentCallback, - ) - } - - checkCompileOnly { GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val googlePayConfiguration: GooglePayConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - GooglePayComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = googlePayConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { IdealComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val idealConfig: IdealConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - IdealComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = idealConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { InstantPaymentComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val instantPaymentConfiguration: InstantPaymentConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - InstantPaymentComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = instantPaymentConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { MBWayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val mbWayConfiguration: MBWayConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - MBWayComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = mbWayConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { MolpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val molpayConfig: MolpayConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - MolpayComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = molpayConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { OnlineBankingCZComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val onlineBankingCZConfig: OnlineBankingCZConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - OnlineBankingCZComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = onlineBankingCZConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { OnlineBankingJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val onlineBankingJPConfig: OnlineBankingJPConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - OnlineBankingJPComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = onlineBankingJPConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { OnlineBankingPLComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val onlineBankingPLConfig: OnlineBankingPLConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - OnlineBankingPLComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = onlineBankingPLConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { OnlineBankingSKComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val onlineBankingSKConfig: OnlineBankingSKConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - OnlineBankingSKComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = onlineBankingSKConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { OpenBankingComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val openBankingConfig: OpenBankingConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - OpenBankingComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = openBankingConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { PayByBankComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val payByBankConfig: PayByBankConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - PayByBankComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = payByBankConfig, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { PayEasyComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val payEasyConfiguration: PayEasyConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - PayEasyComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = payEasyConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { SepaComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val sepaConfiguration: SepaConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - SepaComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = sepaConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { SevenElevenComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val sevenElevenConfiguration: SevenElevenConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - SevenElevenComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = sevenElevenConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - checkCompileOnly { UPIComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - val upiConfiguration: UPIConfiguration = - getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context) - UPIComponentProvider(dropInParams, sessionParams, analyticsRepository).get( - fragment = fragment, - paymentMethod = paymentMethod, - configuration = upiConfiguration, - callback = componentCallback as ComponentCallback, - ) - } - - else -> { - throw CheckoutException("Unable to find component for type - ${paymentMethod.type}") - } - }.apply { - setOnRedirectListener(onRedirect) - } -} - -internal fun DropInConfiguration.mapToParams(amount: Amount?): DropInComponentParams { - return DropInComponentParamsMapper().mapToParams(this, amount) -} - -internal fun getFragmentForStoredPaymentMethod( - storedPaymentMethod: StoredPaymentMethod, - fromPreselected: Boolean -): DropInBottomSheetDialogFragment { - return when { - checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { - CardComponentDialogFragment.newInstance(storedPaymentMethod, fromPreselected) - } - - else -> { - GenericComponentDialogFragment.newInstance(storedPaymentMethod, fromPreselected) - } - } -} - -internal fun getFragmentForPaymentMethod(paymentMethod: PaymentMethod): DropInBottomSheetDialogFragment { - return when { - checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - CardComponentDialogFragment.newInstance(paymentMethod) - } - - checkCompileOnly { BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - BacsDirectDebitDialogFragment.newInstance(paymentMethod) - } - - checkCompileOnly { GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - GiftCardComponentDialogFragment.newInstance(paymentMethod) - } - - checkCompileOnly { GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { - GooglePayComponentDialogFragment.newInstance(paymentMethod) - } - - else -> { - GenericComponentDialogFragment.newInstance(paymentMethod) - } - } -} - -internal inline fun checkCompileOnly(block: () -> Boolean): Boolean { - return runCompileOnly(block) ?: false -} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/ComponentProvider.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/ComponentProvider.kt new file mode 100644 index 0000000000..e3b2cc6aa0 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/ComponentProvider.kt @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2019 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by caiof on 24/4/2019. + */ + +package com.adyen.checkout.dropin.internal.provider + +import androidx.fragment.app.Fragment +import com.adyen.checkout.ach.ACHDirectDebitComponent +import com.adyen.checkout.ach.ACHDirectDebitComponentState +import com.adyen.checkout.ach.internal.provider.ACHDirectDebitComponentProvider +import com.adyen.checkout.bacs.BacsDirectDebitComponent +import com.adyen.checkout.bacs.BacsDirectDebitComponentState +import com.adyen.checkout.bacs.internal.provider.BacsDirectDebitComponentProvider +import com.adyen.checkout.bcmc.BcmcComponent +import com.adyen.checkout.bcmc.BcmcComponentState +import com.adyen.checkout.bcmc.internal.provider.BcmcComponentProvider +import com.adyen.checkout.blik.BlikComponent +import com.adyen.checkout.blik.BlikComponentState +import com.adyen.checkout.blik.internal.provider.BlikComponentProvider +import com.adyen.checkout.boleto.BoletoComponent +import com.adyen.checkout.boleto.BoletoComponentState +import com.adyen.checkout.boleto.internal.provider.BoletoComponentProvider +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.card.CardComponentState +import com.adyen.checkout.card.internal.provider.CardComponentProvider +import com.adyen.checkout.cashapppay.CashAppPayComponent +import com.adyen.checkout.cashapppay.CashAppPayComponentState +import com.adyen.checkout.cashapppay.internal.provider.CashAppPayComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.ComponentCallback +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.components.core.internal.PaymentComponent +import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPComponent +import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPComponentState +import com.adyen.checkout.conveniencestoresjp.internal.provider.ConvenienceStoresJPComponentProvider +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.dotpay.DotpayComponent +import com.adyen.checkout.dotpay.DotpayComponentState +import com.adyen.checkout.dotpay.internal.provider.DotpayComponentProvider +import com.adyen.checkout.dropin.internal.util.checkCompileOnly +import com.adyen.checkout.entercash.EntercashComponent +import com.adyen.checkout.entercash.EntercashComponentState +import com.adyen.checkout.entercash.internal.provider.EntercashComponentProvider +import com.adyen.checkout.eps.EPSComponent +import com.adyen.checkout.eps.EPSComponentState +import com.adyen.checkout.eps.internal.provider.EPSComponentProvider +import com.adyen.checkout.giftcard.GiftCardComponent +import com.adyen.checkout.giftcard.GiftCardComponentCallback +import com.adyen.checkout.giftcard.internal.provider.GiftCardComponentProvider +import com.adyen.checkout.googlepay.GooglePayComponent +import com.adyen.checkout.googlepay.GooglePayComponentState +import com.adyen.checkout.googlepay.internal.provider.GooglePayComponentProvider +import com.adyen.checkout.ideal.IdealComponent +import com.adyen.checkout.ideal.IdealComponentState +import com.adyen.checkout.ideal.internal.provider.IdealComponentProvider +import com.adyen.checkout.instant.InstantComponentState +import com.adyen.checkout.instant.InstantPaymentComponent +import com.adyen.checkout.instant.internal.provider.InstantPaymentComponentProvider +import com.adyen.checkout.mbway.MBWayComponent +import com.adyen.checkout.mbway.MBWayComponentState +import com.adyen.checkout.mbway.internal.provider.MBWayComponentProvider +import com.adyen.checkout.molpay.MolpayComponent +import com.adyen.checkout.molpay.MolpayComponentState +import com.adyen.checkout.molpay.internal.provider.MolpayComponentProvider +import com.adyen.checkout.onlinebankingcz.OnlineBankingCZComponent +import com.adyen.checkout.onlinebankingcz.OnlineBankingCZComponentState +import com.adyen.checkout.onlinebankingcz.internal.provider.OnlineBankingCZComponentProvider +import com.adyen.checkout.onlinebankingjp.OnlineBankingJPComponent +import com.adyen.checkout.onlinebankingjp.OnlineBankingJPComponentState +import com.adyen.checkout.onlinebankingjp.internal.provider.OnlineBankingJPComponentProvider +import com.adyen.checkout.onlinebankingpl.OnlineBankingPLComponent +import com.adyen.checkout.onlinebankingpl.OnlineBankingPLComponentState +import com.adyen.checkout.onlinebankingpl.internal.provider.OnlineBankingPLComponentProvider +import com.adyen.checkout.onlinebankingsk.OnlineBankingSKComponent +import com.adyen.checkout.onlinebankingsk.OnlineBankingSKComponentState +import com.adyen.checkout.onlinebankingsk.internal.provider.OnlineBankingSKComponentProvider +import com.adyen.checkout.openbanking.OpenBankingComponent +import com.adyen.checkout.openbanking.OpenBankingComponentState +import com.adyen.checkout.openbanking.internal.provider.OpenBankingComponentProvider +import com.adyen.checkout.paybybank.PayByBankComponent +import com.adyen.checkout.paybybank.PayByBankComponentState +import com.adyen.checkout.paybybank.internal.provider.PayByBankComponentProvider +import com.adyen.checkout.payeasy.PayEasyComponent +import com.adyen.checkout.payeasy.PayEasyComponentState +import com.adyen.checkout.payeasy.internal.provider.PayEasyComponentProvider +import com.adyen.checkout.sepa.SepaComponent +import com.adyen.checkout.sepa.SepaComponentState +import com.adyen.checkout.sepa.internal.provider.SepaComponentProvider +import com.adyen.checkout.seveneleven.SevenElevenComponent +import com.adyen.checkout.seveneleven.SevenElevenComponentState +import com.adyen.checkout.seveneleven.internal.provider.SevenElevenComponentProvider +import com.adyen.checkout.upi.UPIComponent +import com.adyen.checkout.upi.UPIComponentState +import com.adyen.checkout.upi.internal.provider.UPIComponentProvider + +/** + * Provides a [PaymentComponent] from a [PaymentComponentProvider] using the [StoredPaymentMethod] reference. + * + * @param fragment The Fragment which the PaymentComponent lifecycle will be bound to. + * @param storedPaymentMethod The stored payment method to be parsed. + * @throws CheckoutException In case a component cannot be created. + */ +@Suppress("UNCHECKED_CAST", "LongParameterList") +internal fun getComponentFor( + fragment: Fragment, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + dropInOverrideParams: DropInOverrideParams, + componentCallback: ComponentCallback<*>, + analyticsRepository: AnalyticsRepository, + onRedirect: () -> Unit, +): PaymentComponent { + return when { + checkCompileOnly { ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { + ACHDirectDebitComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + key = storedPaymentMethod.id, + ) + } + + checkCompileOnly { BlikComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { + BlikComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + key = storedPaymentMethod.id, + ) + } + + checkCompileOnly { CashAppPayComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { + CashAppPayComponentProvider(dropInOverrideParams).get( + fragment = fragment, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + key = storedPaymentMethod.id, + ) + } + + checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { + CardComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + key = storedPaymentMethod.id, + ) + } + + else -> { + throw CheckoutException("Unable to find stored component for type - ${storedPaymentMethod.type}") + } + }.apply { + setOnRedirectListener(onRedirect) + } +} + +/** + * Provides a [PaymentComponent] from a [PaymentComponentProvider] using the [PaymentMethod] reference. + * + * @param fragment The Fragment which the PaymentComponent lifecycle will be bound to. + * @param paymentMethod The payment method to be parsed. + * @throws CheckoutException In case a component cannot be created. + */ +@Suppress("LongMethod", "UNCHECKED_CAST", "LongParameterList", "CyclomaticComplexMethod") +internal fun getComponentFor( + fragment: Fragment, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + dropInOverrideParams: DropInOverrideParams, + componentCallback: ComponentCallback<*>, + analyticsRepository: AnalyticsRepository, + onRedirect: () -> Unit, +): PaymentComponent { + return when { + checkCompileOnly { ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + ACHDirectDebitComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + BacsDirectDebitComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { BcmcComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + BcmcComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { BlikComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + BlikComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { BoletoComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + BoletoComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + CardComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { CashAppPayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + CashAppPayComponentProvider(dropInOverrideParams).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { ConvenienceStoresJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + ConvenienceStoresJPComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { DotpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + DotpayComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { EntercashComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + EntercashComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { EPSComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + EPSComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + GiftCardComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as GiftCardComponentCallback, + ) + } + + checkCompileOnly { GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + GooglePayComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { IdealComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + IdealComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { MBWayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + MBWayComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { MolpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + MolpayComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { OnlineBankingCZComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + OnlineBankingCZComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { OnlineBankingJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + OnlineBankingJPComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { OnlineBankingPLComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + OnlineBankingPLComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { OnlineBankingSKComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + OnlineBankingSKComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { OpenBankingComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + OpenBankingComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { PayByBankComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + PayByBankComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { PayEasyComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + PayEasyComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { SepaComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + SepaComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { SevenElevenComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + SevenElevenComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + checkCompileOnly { UPIComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + UPIComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + // InstantPaymentComponent has to be checked last, since it's the only component which doesn't explicitly lists + // which payment methods it supports. Meaning it could take over a payment method that should be handled by + // it's dedicated component. + checkCompileOnly { InstantPaymentComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + InstantPaymentComponentProvider(dropInOverrideParams, analyticsRepository).get( + fragment = fragment, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = componentCallback as ComponentCallback, + ) + } + + else -> { + throw CheckoutException("Unable to find component for type - ${paymentMethod.type}") + } + }.apply { + setOnRedirectListener(onRedirect) + } +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/FragmentProvider.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/FragmentProvider.kt new file mode 100644 index 0000000000..954e795ee4 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/FragmentProvider.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 8/12/2023. + */ + +package com.adyen.checkout.dropin.internal.provider + +import com.adyen.checkout.bacs.BacsDirectDebitComponent +import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.StoredPaymentMethod +import com.adyen.checkout.dropin.internal.ui.BacsDirectDebitDialogFragment +import com.adyen.checkout.dropin.internal.ui.CardComponentDialogFragment +import com.adyen.checkout.dropin.internal.ui.DropInBottomSheetDialogFragment +import com.adyen.checkout.dropin.internal.ui.GenericComponentDialogFragment +import com.adyen.checkout.dropin.internal.ui.GiftCardComponentDialogFragment +import com.adyen.checkout.dropin.internal.ui.GooglePayComponentDialogFragment +import com.adyen.checkout.dropin.internal.util.checkCompileOnly +import com.adyen.checkout.giftcard.GiftCardComponent +import com.adyen.checkout.googlepay.GooglePayComponent + +internal fun getFragmentForStoredPaymentMethod( + storedPaymentMethod: StoredPaymentMethod, + fromPreselected: Boolean +): DropInBottomSheetDialogFragment { + return when { + checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) } -> { + CardComponentDialogFragment.newInstance(storedPaymentMethod, fromPreselected) + } + + else -> { + GenericComponentDialogFragment.newInstance(storedPaymentMethod, fromPreselected) + } + } +} + +internal fun getFragmentForPaymentMethod(paymentMethod: PaymentMethod): DropInBottomSheetDialogFragment { + return when { + checkCompileOnly { CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + CardComponentDialogFragment.newInstance(paymentMethod) + } + + checkCompileOnly { BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + BacsDirectDebitDialogFragment.newInstance(paymentMethod) + } + + checkCompileOnly { GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + GiftCardComponentDialogFragment.newInstance(paymentMethod) + } + + checkCompileOnly { GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) } -> { + GooglePayComponentDialogFragment.newInstance(paymentMethod) + } + + else -> { + GenericComponentDialogFragment.newInstance(paymentMethod) + } + } +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/PaymentMethodAvailabilityProvider.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/PaymentMethodAvailabilityProvider.kt new file mode 100644 index 0000000000..3ecbf65e7d --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/provider/PaymentMethodAvailabilityProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 8/12/2023. + */ + +package com.adyen.checkout.dropin.internal.provider + +import android.app.Application +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.ComponentAvailableCallback +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.AlwaysAvailablePaymentMethod +import com.adyen.checkout.components.core.internal.NotAvailablePaymentMethod +import com.adyen.checkout.components.core.internal.PaymentMethodAvailabilityCheck +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.core.internal.util.runCompileOnly +import com.adyen.checkout.googlepay.internal.provider.GooglePayComponentProvider +import com.adyen.checkout.wechatpay.WeChatPayProvider + +@Suppress("LongParameterList") +internal fun checkPaymentMethodAvailability( + application: Application, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + dropInOverrideParams: DropInOverrideParams, + callback: ComponentAvailableCallback, +) { + try { + adyenLog(AdyenLogLevel.VERBOSE, "checkPaymentMethodAvailability") { + "Checking availability for type - ${paymentMethod.type}" + } + + val type = paymentMethod.type ?: throw CheckoutException("PaymentMethod type is null") + + val availabilityCheck = getPaymentMethodAvailabilityCheck(type, dropInOverrideParams) + + availabilityCheck.isAvailable(application, paymentMethod, checkoutConfiguration, callback) + } catch (e: CheckoutException) { + adyenLog(AdyenLogLevel.ERROR, "checkPaymentMethodAvailability", e) { + "Unable to initiate ${paymentMethod.type}" + } + callback.onAvailabilityResult(false, paymentMethod) + } +} + +/** + * Provides the [PaymentMethodAvailabilityCheck] class for the specified [paymentMethodType], if available. + */ +private fun getPaymentMethodAvailabilityCheck( + paymentMethodType: String, + dropInOverrideParams: DropInOverrideParams, +): PaymentMethodAvailabilityCheck<*> { + val availabilityCheck = when (paymentMethodType) { + PaymentMethodTypes.GOOGLE_PAY, + PaymentMethodTypes.GOOGLE_PAY_LEGACY -> runCompileOnly { + GooglePayComponentProvider(dropInOverrideParams) + } + + PaymentMethodTypes.WECHAT_PAY_SDK -> runCompileOnly { WeChatPayProvider() } + else -> AlwaysAvailablePaymentMethod() + } + + return availabilityCheck ?: NotAvailablePaymentMethod() +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInService.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInService.kt index 7cb0c15ad9..cfd44ddf3e 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInService.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInService.kt @@ -18,10 +18,12 @@ import android.os.Bundle import android.os.IBinder import androidx.annotation.RestrictTo import com.adyen.checkout.card.BinLookupData +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.dropin.AddressLookupDropInServiceResult import com.adyen.checkout.dropin.BalanceDropInServiceResult import com.adyen.checkout.dropin.BaseDropInServiceContract import com.adyen.checkout.dropin.BaseDropInServiceResult @@ -59,7 +61,7 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI } override fun onBind(intent: Intent?): IBinder { - Logger.d(TAG, "onBind") + adyenLog(AdyenLogLevel.DEBUG) { "onBind" } if (intent?.hasExtra(INTENT_EXTRA_ADDITIONAL_DATA) == true) { additionalData = intent.getBundleExtra(INTENT_EXTRA_ADDITIONAL_DATA) } @@ -67,22 +69,22 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI } override fun onUnbind(intent: Intent?): Boolean { - Logger.d(TAG, "onUnbind") + adyenLog(AdyenLogLevel.DEBUG) { "onUnbind" } return super.onUnbind(intent) } override fun onRebind(intent: Intent?) { - Logger.d(TAG, "onRebind") + adyenLog(AdyenLogLevel.DEBUG) { "onRebind" } super.onRebind(intent) } override fun onCreate() { - Logger.d(TAG, "onCreate") + adyenLog(AdyenLogLevel.DEBUG) { "onCreate" } super.onCreate() } override fun onDestroy() { - Logger.d(TAG, "onDestroy") + adyenLog(AdyenLogLevel.DEBUG) { "onDestroy" } cancel() @@ -90,22 +92,27 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI } final override fun sendResult(result: DropInServiceResult) { - Logger.d(TAG, "dispatching DropInServiceResult") + adyenLog(AdyenLogLevel.DEBUG) { "dispatching DropInServiceResult" } emitResult(result) } final override fun sendBalanceResult(result: BalanceDropInServiceResult) { - Logger.d(TAG, "dispatching BalanceDropInServiceResult") + adyenLog(AdyenLogLevel.DEBUG) { "dispatching BalanceDropInServiceResult" } emitResult(result) } final override fun sendOrderResult(result: OrderDropInServiceResult) { - Logger.d(TAG, "dispatching OrderDropInServiceResult") + adyenLog(AdyenLogLevel.DEBUG) { "dispatching OrderDropInServiceResult" } emitResult(result) } final override fun sendRecurringResult(result: RecurringDropInServiceResult) { - Logger.d(TAG, "dispatching RecurringDropInServiceResult") + adyenLog(AdyenLogLevel.DEBUG) { "dispatching RecurringDropInServiceResult" } + emitResult(result) + } + + final override fun sendAddressLookupResult(result: AddressLookupDropInServiceResult) { + adyenLog(AdyenLogLevel.DEBUG) { "dispatching AddressLookupDropInServiceResult" } emitResult(result) } @@ -121,7 +128,7 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI } final override fun requestRemoveStoredPaymentMethod(storedPaymentMethod: StoredPaymentMethod) { - Logger.d(TAG, "requestRemoveStoredPaymentMethod") + adyenLog(AdyenLogLevel.DEBUG) { "requestRemoveStoredPaymentMethod" } onRemoveStoredPaymentMethod(storedPaymentMethod) } @@ -141,6 +148,14 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI onBinLookup(data) } + final override fun onAddressLookupQueryChangedCalled(query: String) { + onAddressLookupQueryChanged(query) + } + + final override fun onAddressLookupCompletionCalled(lookupAddress: LookupAddress): Boolean { + return onAddressLookupCompletion(lookupAddress) + } + internal class DropInBinder(service: BaseDropInService) : Binder() { private val serviceRef: WeakReference = WeakReference(service) @@ -150,8 +165,6 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI companion object { - private val TAG = LogUtil.getTag() - private const val INTENT_EXTRA_ADDITIONAL_DATA = "ADDITIONAL_DATA" internal fun startService( @@ -160,11 +173,11 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI merchantService: ComponentName, additionalData: Bundle?, ): Boolean { - Logger.d(TAG, "startService - ${context::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "startService - ${context::class.simpleName}" } val intent = Intent().apply { component = merchantService } - Logger.d(TAG, "merchant service: ${merchantService.className}") + adyenLog(AdyenLogLevel.DEBUG) { "merchant service: ${merchantService.className}" } context.startService(intent) return bindService(context, connection, merchantService, additionalData) } @@ -175,7 +188,7 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI merchantService: ComponentName, additionalData: Bundle?, ): Boolean { - Logger.d(TAG, "bindService - ${context::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "bindService - ${context::class.simpleName}" } val intent = Intent().apply { component = merchantService putExtra(INTENT_EXTRA_ADDITIONAL_DATA, additionalData) @@ -190,7 +203,7 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI ) { unbindService(context, connection) - Logger.d(TAG, "stopService - ${context::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "stopService - ${context::class.simpleName}" } val intent = Intent().apply { component = merchantService @@ -199,7 +212,7 @@ constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropI } private fun unbindService(context: Context, connection: ServiceConnection) { - Logger.d(TAG, "unbindService - ${context::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "unbindService - ${context::class.simpleName}" } context.unbindService(connection) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInServiceInterfaces.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInServiceInterfaces.kt index 7a17d22dd3..c464076a6e 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInServiceInterfaces.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/service/BaseDropInServiceInterfaces.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.dropin.internal.service import com.adyen.checkout.card.BinLookupData import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.StoredPaymentMethod @@ -17,6 +18,7 @@ import com.adyen.checkout.core.Environment import com.adyen.checkout.dropin.BaseDropInServiceResult import com.adyen.checkout.sessions.core.SessionModel +@Suppress("TooManyFunctions") internal interface BaseDropInServiceInterface { suspend fun observeResult(callback: (BaseDropInServiceResult) -> Unit) fun requestPaymentsCall(paymentComponentState: PaymentComponentState<*>) @@ -28,6 +30,8 @@ internal interface BaseDropInServiceInterface { fun onRedirectCalled() fun onBinValueCalled(binValue: String) fun onBinLookupCalled(data: List) + fun onAddressLookupQueryChangedCalled(query: String) + fun onAddressLookupCompletionCalled(lookupAddress: LookupAddress): Boolean } internal interface SessionDropInServiceInterface : BaseDropInServiceInterface { diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt index 151130e303..29a5abf14e 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/ActionComponentDialogFragment.kt @@ -21,22 +21,21 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.adyen.checkout.action.core.GenericActionComponent -import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider import com.adyen.checkout.components.core.ActionComponentCallback import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.action.Action -import com.adyen.checkout.components.core.internal.util.toast +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CancellationException import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.exception.PermissionException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.databinding.FragmentGenericActionComponentBinding -import com.adyen.checkout.dropin.internal.provider.mapToParams import com.adyen.checkout.dropin.internal.util.arguments +import com.google.android.material.bottomsheet.BottomSheetBehavior import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -51,25 +50,35 @@ internal class ActionComponentDialogFragment : private val actionComponentViewModel: ActionComponentViewModel by viewModels() private val action: Action by arguments(ACTION) - private val actionConfiguration: GenericActionConfiguration by arguments(ACTION_CONFIGURATION) + private val checkoutConfiguration: CheckoutConfiguration by arguments(CHECKOUT_CONFIGURATION) private lateinit var actionComponent: GenericActionComponent + private var permissionCallback: PermissionHandlerCallback? = null + private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> - if (!isGranted) { - requireContext().toast(getString(R.string.checkout_permission_not_granted)) - } else { - // TODO: trigger download image flow when user accept storage permission after checking permission type + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { resultsMap -> + resultsMap.firstNotNullOf { result -> + val requestedPermission = result.key + val isGranted = result.value + if (isGranted) { + adyenLog(AdyenLogLevel.DEBUG) { "Permission $requestedPermission granted" } + permissionCallback?.onPermissionGranted(requestedPermission) + } else { + adyenLog(AdyenLogLevel.DEBUG) { "Permission $requestedPermission denied" } + permissionCallback?.onPermissionDenied(requestedPermission) + } + permissionCallback = null } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Logger.d(TAG, "onCreate") + adyenLog(AdyenLogLevel.DEBUG) { "onCreate" } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentGenericActionComponentBinding.inflate(inflater) + setInitViewState(BottomSheetBehavior.STATE_EXPANDED) return binding.root } @@ -79,15 +88,15 @@ internal class ActionComponentDialogFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } initObservers() binding.header.isVisible = false try { - val componentParams = dropInViewModel.dropInConfiguration.mapToParams(dropInViewModel.amount) - actionComponent = GenericActionComponentProvider(componentParams).get( + val dropInOverrideParams = dropInViewModel.getDropInOverrideParams() + actionComponent = GenericActionComponentProvider(dropInOverrideParams).get( fragment = this, - configuration = actionConfiguration, + checkoutConfiguration = checkoutConfiguration, callback = this, ) @@ -111,10 +120,24 @@ internal class ActionComponentDialogFragment : } override fun onError(componentError: ComponentError) { - Logger.d(TAG, "onError") + adyenLog(AdyenLogLevel.DEBUG) { "onError" } handleError(componentError) } + override fun onPermissionRequest(requiredPermission: String, permissionCallback: PermissionHandlerCallback) { + this.permissionCallback = permissionCallback + adyenLog(AdyenLogLevel.DEBUG) { "Permission request information dialog shown" } + AlertDialog.Builder(requireContext()) + .setTitle(R.string.checkout_rationale_title_storage_permission) + .setMessage(R.string.checkout_rationale_message_storage_permission) + .setOnDismissListener { + adyenLog(AdyenLogLevel.DEBUG) { "Permission $requiredPermission requested" } + requestPermissionLauncher.launch(arrayOf(requiredPermission)) + } + .setPositiveButton(R.string.error_dialog_button) { dialog, _ -> dialog.dismiss() } + .show() + } + private fun initObservers() { actionComponentViewModel.eventsFlow .flowWithLifecycle(viewLifecycleOwner.lifecycle) @@ -147,7 +170,7 @@ internal class ActionComponentDialogFragment : } override fun onCancel(dialog: DialogInterface) { - Logger.d(TAG, "onCancel") + adyenLog(AdyenLogLevel.DEBUG) { "onCancel" } if (shouldFinishWithAction()) { protocol.finishWithAction() } else { @@ -156,34 +179,21 @@ internal class ActionComponentDialogFragment : } private fun onActionComponentDataChanged(actionComponentData: ActionComponentData?) { - Logger.d(TAG, "onActionComponentDataChanged") + adyenLog(AdyenLogLevel.DEBUG) { "onActionComponentDataChanged" } if (actionComponentData != null) { protocol.requestDetailsCall(actionComponentData) } } private fun handleError(componentError: ComponentError) { - when (val exception = componentError.exception) { + when (componentError.exception) { is CancellationException -> { - Logger.d(TAG, "Flow was cancelled by user") + adyenLog(AdyenLogLevel.DEBUG) { "Flow was cancelled by user" } onBackPressed() } - is PermissionException -> { - val requiredPermission = exception.requiredPermission - Logger.e(TAG, exception.message.orEmpty(), exception) - // TODO: checkout_rationale_title_storage_permission and checkout_rationale_message_storage_permission - // TODO: can be reused based on required permission - AlertDialog.Builder(requireContext()) - .setTitle(R.string.checkout_rationale_title_storage_permission) - .setMessage(R.string.checkout_rationale_message_storage_permission) - .setOnDismissListener { requestPermissionLauncher.launch(requiredPermission) } - .setPositiveButton(R.string.error_dialog_button) { dialog, _ -> dialog.dismiss() } - .show() - } - else -> { - Logger.e(TAG, componentError.errorMessage) + adyenLog(AdyenLogLevel.ERROR) { componentError.errorMessage } protocol.showError(null, getString(R.string.action_failed), componentError.errorMessage, true) } } @@ -194,7 +204,7 @@ internal class ActionComponentDialogFragment : } fun handleIntent(intent: Intent) { - Logger.d(TAG, "handleAction") + adyenLog(AdyenLogLevel.DEBUG) { "handleAction" } actionComponent.handleIntent(intent) } @@ -204,18 +214,16 @@ internal class ActionComponentDialogFragment : } companion object { - private val TAG = LogUtil.getTag() - const val ACTION = "ACTION" - const val ACTION_CONFIGURATION = "ACTION_CONFIGURATION" + const val CHECKOUT_CONFIGURATION = "CHECKOUT_CONFIGURATION" fun newInstance( action: Action, - actionConfiguration: GenericActionConfiguration + checkoutConfiguration: CheckoutConfiguration, ): ActionComponentDialogFragment { val args = Bundle() args.putParcelable(ACTION, action) - args.putParcelable(ACTION_CONFIGURATION, actionConfiguration) + args.putParcelable(CHECKOUT_CONFIGURATION, checkoutConfiguration) val componentDialogFragment = ActionComponentDialogFragment() componentDialogFragment.arguments = args diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BacsDirectDebitDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BacsDirectDebitDialogFragment.kt index c1ed3395e7..33c43ed4d1 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BacsDirectDebitDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BacsDirectDebitDialogFragment.kt @@ -16,8 +16,8 @@ import android.view.ViewGroup import android.view.WindowManager import android.widget.FrameLayout import com.adyen.checkout.bacs.BacsDirectDebitComponent -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.databinding.FragmentBacsDirectDebitComponentBinding import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog @@ -29,9 +29,7 @@ internal class BacsDirectDebitDialogFragment : BaseComponentDialogFragment() { private val bacsDirectDebitComponent: BacsDirectDebitComponent by lazy { component as BacsDirectDebitComponent } - companion object : BaseCompanion(BacsDirectDebitDialogFragment::class.java) { - private val TAG = LogUtil.getTag() - } + companion object : BaseCompanion(BacsDirectDebitDialogFragment::class.java) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = FragmentBacsDirectDebitComponentBinding.inflate(inflater, container, false) @@ -40,7 +38,7 @@ internal class BacsDirectDebitDialogFragment : BaseComponentDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } binding.header.text = paymentMethod.name binding.bacsView.attach(bacsDirectDebitComponent, viewLifecycleOwner) @@ -52,7 +50,7 @@ internal class BacsDirectDebitDialogFragment : BaseComponentDialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - Logger.d(TAG, "onCreateDialog") + adyenLog(AdyenLogLevel.DEBUG) { "onCreateDialog" } val dialog = super.onCreateDialog(savedInstanceState) setDialogToFullScreen(dialog) return dialog diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt index 0984e20207..c5b60aaae2 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/BaseComponentDialogFragment.kt @@ -20,9 +20,9 @@ import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.internal.provider.getComponentFor @@ -35,10 +35,6 @@ internal abstract class BaseComponentDialogFragment : DropInBottomSheetDialogFragment(), ComponentCallback> { - companion object { - private val TAG = LogUtil.getTag() - } - var paymentMethod: PaymentMethod = PaymentMethod() var storedPaymentMethod: StoredPaymentMethod = StoredPaymentMethod() lateinit var component: PaymentComponent @@ -89,7 +85,7 @@ internal abstract class BaseComponentDialogFragment : ): View? override fun onBackPressed(): Boolean { - Logger.d(TAG, "onBackPressed - $navigatedFromPreselected") + adyenLog(AdyenLogLevel.DEBUG) { "onBackPressed - $navigatedFromPreselected" } when { navigatedFromPreselected -> protocol.showPreselectedDialog() @@ -110,10 +106,9 @@ internal abstract class BaseComponentDialogFragment : getComponentFor( fragment = this, storedPaymentMethod = storedPaymentMethod, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), componentCallback = this, - sessionDetails = dropInViewModel.sessionDetails, analyticsRepository = dropInViewModel.analyticsRepository, onRedirect = protocol::onRedirect, ) @@ -121,9 +116,8 @@ internal abstract class BaseComponentDialogFragment : getComponentFor( fragment = this, paymentMethod = paymentMethod, - sessionDetails = dropInViewModel.sessionDetails, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), componentCallback = this, analyticsRepository = dropInViewModel.analyticsRepository, onRedirect = protocol::onRedirect, @@ -163,12 +157,12 @@ internal abstract class BaseComponentDialogFragment : } private fun onComponentError(componentError: ComponentError) { - Logger.e(TAG, "ComponentError", componentError.exception) + adyenLog(AdyenLogLevel.ERROR, componentError.exception) { "ComponentError" } handleError(componentError) } fun handleError(componentError: ComponentError) { - Logger.e(TAG, componentError.errorMessage) + adyenLog(AdyenLogLevel.ERROR) { componentError.errorMessage } protocol.showError(null, getString(R.string.component_error), componentError.errorMessage, true) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt index 4e290db11f..b5d1b0801b 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/CardComponentDialogFragment.kt @@ -12,13 +12,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.viewModelScope import com.adyen.checkout.card.CardComponent -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.databinding.FragmentCardComponentBinding import com.google.android.material.bottomsheet.BottomSheetBehavior +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach -internal class CardComponentDialogFragment : BaseComponentDialogFragment() { +internal class CardComponentDialogFragment : BaseComponentDialogFragment(), AddressLookupCallback { private var _binding: FragmentCardComponentBinding? = null private val binding: FragmentCardComponentBinding get() = requireNotNull(_binding) @@ -32,7 +37,7 @@ internal class CardComponentDialogFragment : BaseComponentDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } binding.header.text = if (isStoredPayment) { storedPaymentMethod.name @@ -42,6 +47,7 @@ internal class CardComponentDialogFragment : BaseComponentDialogFragment() { cardComponent.setOnBinValueListener(protocol::onBinValue) cardComponent.setOnBinLookupListener(protocol::onBinLookup) + cardComponent.setAddressLookupCallback(this) binding.cardView.attach(cardComponent, viewLifecycleOwner) @@ -49,6 +55,29 @@ internal class CardComponentDialogFragment : BaseComponentDialogFragment() { setInitViewState(BottomSheetBehavior.STATE_EXPANDED) binding.cardView.requestFocus() } + + dropInViewModel.addressLookupOptionsFlow.onEach { + cardComponent.updateAddressLookupOptions(it) + }.launchIn(dropInViewModel.viewModelScope) + + dropInViewModel.addressLookupCompleteFlow.onEach { + cardComponent.setAddressLookupResult(it) + }.launchIn(dropInViewModel.viewModelScope) + } + + override fun onQueryChanged(query: String) { + protocol.onAddressLookupQuery(query) + } + + override fun onLookupCompletion(lookupAddress: LookupAddress): Boolean { + return protocol.onAddressLookupCompletion(lookupAddress) + } + + override fun onBackPressed(): Boolean { + if (cardComponent.handleBackPress()) { + return true + } + return super.onBackPressed() } override fun onDestroyView() { @@ -56,7 +85,5 @@ internal class CardComponentDialogFragment : BaseComponentDialogFragment() { super.onDestroyView() } - companion object : BaseCompanion(CardComponentDialogFragment::class.java) { - private val TAG = LogUtil.getTag() - } + companion object : BaseCompanion(CardComponentDialogFragment::class.java) } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt index b4ac5e4857..15dec810c6 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInActivity.kt @@ -25,6 +25,8 @@ import androidx.lifecycle.repeatOnLifecycle import com.adyen.checkout.card.BinLookupData import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.BalanceResult +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.PaymentComponentState @@ -33,12 +35,12 @@ import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.util.createLocalizedContext -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.dropin.AddressLookupDropInServiceResult import com.adyen.checkout.dropin.BalanceDropInServiceResult import com.adyen.checkout.dropin.BaseDropInServiceResult import com.adyen.checkout.dropin.DropIn -import com.adyen.checkout.dropin.DropInConfiguration import com.adyen.checkout.dropin.DropInServiceResult import com.adyen.checkout.dropin.DropInServiceResultError import com.adyen.checkout.dropin.OrderDropInServiceResult @@ -46,7 +48,6 @@ import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.RecurringDropInServiceResult import com.adyen.checkout.dropin.SessionDropInServiceResult import com.adyen.checkout.dropin.databinding.ActivityDropInBinding -import com.adyen.checkout.dropin.internal.provider.checkCompileOnly import com.adyen.checkout.dropin.internal.provider.getFragmentForPaymentMethod import com.adyen.checkout.dropin.internal.provider.getFragmentForStoredPaymentMethod import com.adyen.checkout.dropin.internal.service.BaseDropInService @@ -56,6 +57,7 @@ import com.adyen.checkout.dropin.internal.ui.model.DropInActivityEvent import com.adyen.checkout.dropin.internal.ui.model.DropInDestination import com.adyen.checkout.dropin.internal.ui.model.GiftCardPaymentConfirmationData import com.adyen.checkout.dropin.internal.util.DropInPrefs +import com.adyen.checkout.dropin.internal.util.checkCompileOnly import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.redirect.RedirectComponent import com.adyen.checkout.sessions.core.CheckoutSession @@ -86,7 +88,7 @@ internal class DropInActivity : private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, binder: IBinder) { - Logger.d(TAG, "onServiceConnected") + adyenLog(AdyenLogLevel.DEBUG) { "onServiceConnected" } val dropInBinder = binder as? BaseDropInService.DropInBinder ?: return dropInService = dropInBinder.getService() @@ -101,47 +103,47 @@ internal class DropInActivity : } paymentDataQueue?.let { - Logger.d(TAG, "Sending queued payment request") + adyenLog(AdyenLogLevel.DEBUG) { "Sending queued payment request" } requestPaymentsCall(it) paymentDataQueue = null } actionDataQueue?.let { - Logger.d(TAG, "Sending queued action request") + adyenLog(AdyenLogLevel.DEBUG) { "Sending queued action request" } requestDetailsCall(it) actionDataQueue = null } balanceDataQueue?.let { - Logger.d(TAG, "Sending queued action request") + adyenLog(AdyenLogLevel.DEBUG) { "Sending queued action request" } requestBalanceCall(it) balanceDataQueue = null } orderDataQueue?.let { - Logger.d(TAG, "Sending queued order request") + adyenLog(AdyenLogLevel.DEBUG) { "Sending queued order request" } requestOrdersCall() orderDataQueue = null } orderCancellationQueue?.let { - Logger.d(TAG, "Sending queued cancel order request") + adyenLog(AdyenLogLevel.DEBUG) { "Sending queued cancel order request" } requestCancelOrderCall(it, true) orderCancellationQueue = null } } override fun onServiceDisconnected(className: ComponentName) { - Logger.d(TAG, "onServiceDisconnected") + adyenLog(AdyenLogLevel.DEBUG) { "onServiceDisconnected" } dropInService = null } } override fun attachBaseContext(newBase: Context?) { - Logger.d(TAG, "attachBaseContext") + adyenLog(AdyenLogLevel.DEBUG) { "attachBaseContext" } super.attachBaseContext(createLocalizedContext(newBase)) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Logger.d(TAG, "onCreate - $savedInstanceState") + adyenLog(AdyenLogLevel.DEBUG) { "onCreate - $savedInstanceState" } val binding = ActivityDropInBinding.inflate(layoutInflater) setContentView(binding.root) overridePendingTransition(0, 0) @@ -170,17 +172,17 @@ internal class DropInActivity : getFragmentByTag(GIFT_CARD_PAYMENT_CONFIRMATION_FRAGMENT_TAG) == null } + @Suppress("ReturnCount") private fun createLocalizedContext(baseContext: Context?): Context? { if (baseContext == null) return null // We need to get the Locale from sharedPrefs because attachBaseContext is called before onCreate, so we don't // have the Config object yet. - val locale = DropInPrefs.getShopperLocale(baseContext) + val locale = DropInPrefs.getShopperLocale(baseContext) ?: return baseContext return baseContext.createLocalizedContext(locale) } @Deprecated("Deprecated in Java") - @Suppress("deprecation") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) checkGooglePayActivityResult(requestCode, resultCode, data) @@ -190,7 +192,7 @@ internal class DropInActivity : if (requestCode != GOOGLE_PAY_REQUEST_CODE) return val fragment = getFragmentByTag(COMPONENT_FRAGMENT_TAG) as? GooglePayComponentDialogFragment if (fragment == null) { - Logger.e(TAG, "GooglePayComponentDialogFragment is not loaded") + adyenLog(AdyenLogLevel.ERROR) { "GooglePayComponentDialogFragment is not loaded" } return } fragment.handleActivityResult(resultCode, data) @@ -198,16 +200,16 @@ internal class DropInActivity : override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - Logger.d(TAG, "onNewIntent") + adyenLog(AdyenLogLevel.DEBUG) { "onNewIntent" } if (intent != null) { handleIntent(intent) } else { - Logger.e(TAG, "Null intent") + adyenLog(AdyenLogLevel.ERROR) { "Null intent" } } } override fun onStart() { - Logger.v(TAG, "onStart") + adyenLog(AdyenLogLevel.VERBOSE) { "onStart" } super.onStart() } @@ -216,21 +218,20 @@ internal class DropInActivity : context = this, connection = serviceConnection, merchantService = dropInViewModel.serviceComponentName, - additionalData = dropInViewModel.dropInConfiguration.additionalDataForDropInService, + additionalData = dropInViewModel.dropInParams.additionalDataForDropInService, ) if (bound) { serviceBound = true } else { - Logger.e( - TAG, + adyenLog(AdyenLogLevel.ERROR) { "Error binding to ${dropInViewModel.serviceComponentName.className}. " + "The system couldn't find the service or your client doesn't have permission to bind to it" - ) + } } } override fun onStop() { - Logger.v(TAG, "onStop") + adyenLog(AdyenLogLevel.VERBOSE) { "onStop" } super.onStop() } @@ -246,9 +247,9 @@ internal class DropInActivity : } override fun requestPaymentsCall(paymentComponentState: PaymentComponentState<*>) { - Logger.d(TAG, "requestPaymentsCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestPaymentsCall" } if (dropInService == null) { - Logger.e(TAG, "service is disconnected, adding to queue") + adyenLog(AdyenLogLevel.ERROR) { "service is disconnected, adding to queue" } paymentDataQueue = paymentComponentState return } @@ -259,9 +260,9 @@ internal class DropInActivity : } override fun requestDetailsCall(actionComponentData: ActionComponentData) { - Logger.d(TAG, "requestDetailsCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestDetailsCall" } if (dropInService == null) { - Logger.e(TAG, "service is disconnected, adding to queue") + adyenLog(AdyenLogLevel.ERROR) { "service is disconnected, adding to queue" } actionDataQueue = actionComponentData return } @@ -271,7 +272,7 @@ internal class DropInActivity : } override fun showError(dialogTitle: String?, errorMessage: String, reason: String, terminate: Boolean) { - Logger.d(TAG, "showError - message: $errorMessage") + adyenLog(AdyenLogLevel.DEBUG) { "showError - message: $errorMessage" } val title = dialogTitle ?: getString(R.string.error_dialog_title) showDialog(title, errorMessage) { errorDialogDismissed(reason, terminate) @@ -287,38 +288,38 @@ internal class DropInActivity : } override fun onResume() { - Logger.v(TAG, "onResume") + adyenLog(AdyenLogLevel.VERBOSE) { "onResume" } super.onResume() setLoading(dropInViewModel.isWaitingResult) } override fun onDestroy() { - Logger.v(TAG, "onDestroy") + adyenLog(AdyenLogLevel.VERBOSE) { "onDestroy" } super.onDestroy() } override fun showPreselectedDialog() { - Logger.d(TAG, "showPreselectedDialog") + adyenLog(AdyenLogLevel.DEBUG) { "showPreselectedDialog" } hideAllScreens() PreselectedStoredPaymentMethodFragment.newInstance(dropInViewModel.getPreselectedStoredPaymentMethod()) .show(supportFragmentManager, PRESELECTED_PAYMENT_METHOD_FRAGMENT_TAG) } override fun showPaymentMethodsDialog() { - Logger.d(TAG, "showPaymentMethodsDialog") + adyenLog(AdyenLogLevel.DEBUG) { "showPaymentMethodsDialog" } hideAllScreens() PaymentMethodListDialogFragment().show(supportFragmentManager, PAYMENT_METHODS_LIST_FRAGMENT_TAG) } override fun showStoredComponentDialog(storedPaymentMethod: StoredPaymentMethod, fromPreselected: Boolean) { - Logger.d(TAG, "showStoredComponentDialog") + adyenLog(AdyenLogLevel.DEBUG) { "showStoredComponentDialog" } hideAllScreens() val dialogFragment = getFragmentForStoredPaymentMethod(storedPaymentMethod, fromPreselected) dialogFragment.show(supportFragmentManager, COMPONENT_FRAGMENT_TAG) } override fun showComponentDialog(paymentMethod: PaymentMethod) { - Logger.d(TAG, "showComponentDialog") + adyenLog(AdyenLogLevel.DEBUG) { "showComponentDialog" } hideAllScreens() val dialogFragment = getFragmentForPaymentMethod(paymentMethod) dialogFragment.show(supportFragmentManager, COMPONENT_FRAGMENT_TAG) @@ -333,15 +334,15 @@ internal class DropInActivity : } override fun terminateDropIn() { - Logger.d(TAG, "terminateDropIn") + adyenLog(AdyenLogLevel.DEBUG) { "terminateDropIn" } dropInViewModel.cancelDropIn() } override fun requestBalanceCall(giftCardComponentState: GiftCardComponentState) { - Logger.d(TAG, "requestCheckBalanceCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestCheckBalanceCall" } dropInViewModel.onBalanceCallRequested(giftCardComponentState) ?: return if (dropInService == null) { - Logger.e(TAG, "requestBalanceCall - service is disconnected") + adyenLog(AdyenLogLevel.ERROR) { "requestBalanceCall - service is disconnected" } balanceDataQueue = giftCardComponentState return } @@ -351,9 +352,9 @@ internal class DropInActivity : } private fun requestOrdersCall() { - Logger.d(TAG, "requestOrdersCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestOrdersCall" } if (dropInService == null) { - Logger.e(TAG, "requestOrdersCall - service is disconnected") + adyenLog(AdyenLogLevel.ERROR) { "requestOrdersCall - service is disconnected" } orderDataQueue = Unit return } @@ -363,9 +364,9 @@ internal class DropInActivity : } private fun requestCancelOrderCall(order: OrderRequest, isDropInCancelledByUser: Boolean) { - Logger.d(TAG, "requestCancelOrderCall") + adyenLog(AdyenLogLevel.DEBUG) { "requestCancelOrderCall" } if (dropInService == null) { - Logger.e(TAG, "requestOrdersCall - service is disconnected") + adyenLog(AdyenLogLevel.ERROR) { "requestOrdersCall - service is disconnected" } orderCancellationQueue = order return } @@ -375,7 +376,7 @@ internal class DropInActivity : } override fun finishWithAction() { - Logger.d(TAG, "finishWithActionCall") + adyenLog(AdyenLogLevel.DEBUG) { "finishWithActionCall" } sendResult(DropIn.FINISHED_WITH_ACTION) } @@ -385,7 +386,7 @@ internal class DropInActivity : } private fun handleDropInServiceResult(dropInServiceResult: BaseDropInServiceResult) { - Logger.d(TAG, "handleDropInServiceResult - ${dropInServiceResult::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "handleDropInServiceResult - ${dropInServiceResult::class.simpleName}" } dropInViewModel.isWaitingResult = false when (dropInServiceResult) { is DropInServiceResult -> handleDropInServiceResult(dropInServiceResult) @@ -393,6 +394,7 @@ internal class DropInActivity : is OrderDropInServiceResult -> handleDropInServiceResult(dropInServiceResult) is RecurringDropInServiceResult -> handleDropInServiceResult(dropInServiceResult) is SessionDropInServiceResult -> handleDropInServiceResult(dropInServiceResult) + is AddressLookupDropInServiceResult -> handleDropInServiceResult(dropInServiceResult) } } @@ -403,7 +405,7 @@ internal class DropInActivity : is DropInServiceResult.Update -> handlePaymentMethodsUpdate(dropInServiceResult) is DropInServiceResult.Error -> handleErrorDropInServiceResult(dropInServiceResult) is DropInServiceResult.ToPaymentMethodsList -> dropInViewModel.onToPaymentMethodsList( - dropInServiceResult.paymentMethodsApiResponse + dropInServiceResult.paymentMethodsApiResponse, ) } } @@ -431,6 +433,14 @@ internal class DropInActivity : } } + private fun handleDropInServiceResult(dropInServiceResult: AddressLookupDropInServiceResult) { + when (dropInServiceResult) { + is AddressLookupDropInServiceResult.LookupResult -> handleAddressLookupOptionsUpdate(dropInServiceResult) + is AddressLookupDropInServiceResult.LookupComplete -> handleAddressLookupComplete(dropInServiceResult) + is AddressLookupDropInServiceResult.Error -> handleErrorDropInServiceResult(dropInServiceResult) + } + } + private fun handleDropInServiceResult(dropInServiceResult: SessionDropInServiceResult) { when (dropInServiceResult) { is SessionDropInServiceResult.SessionDataChanged -> @@ -446,10 +456,10 @@ internal class DropInActivity : private fun handleErrorDropInServiceResult(dropInServiceResult: DropInServiceResultError) { val reason = dropInServiceResult.reason ?: "Unspecified reason" - Logger.d(TAG, "handleDropInServiceResult ERROR - reason: $reason") + adyenLog(AdyenLogLevel.DEBUG) { "handleDropInServiceResult ERROR - reason: $reason" } dropInServiceResult.errorDialog?.let { errorDialog -> - val errorMessage = errorDialog.message ?: getString(R.string.payment_failed) + val errorMessage = errorDialog.message ?: getString(R.string.unknown_error) showError(errorDialog.title, errorMessage, reason, dropInServiceResult.dismissDropIn) } ?: if (dropInServiceResult.dismissDropIn) { terminateWithError(reason) @@ -469,21 +479,28 @@ internal class DropInActivity : } private fun handleAction(action: Action) { - Logger.d(TAG, "showActionDialog") + adyenLog(AdyenLogLevel.DEBUG) { "showActionDialog" } setLoading(false) hideAllScreens() - val actionConfiguration = dropInViewModel.dropInConfiguration.genericActionConfiguration - val actionFragment = ActionComponentDialogFragment.newInstance(action, actionConfiguration) + val actionFragment = ActionComponentDialogFragment.newInstance(action, dropInViewModel.checkoutConfiguration) actionFragment.show(supportFragmentManager, ACTION_FRAGMENT_TAG) } private fun handlePaymentMethodsUpdate(dropInServiceResult: DropInServiceResult.Update) { dropInViewModel.handlePaymentMethodsUpdate( dropInServiceResult.paymentMethodsApiResponse, - dropInServiceResult.order + dropInServiceResult.order, ) } + private fun handleAddressLookupOptionsUpdate(lookupResult: AddressLookupDropInServiceResult.LookupResult) { + dropInViewModel.onAddressLookupOptions(lookupResult.options) + } + + private fun handleAddressLookupComplete(lookupResult: AddressLookupDropInServiceResult.LookupComplete) { + dropInViewModel.onAddressLookupComplete(lookupResult.lookupAddress) + } + private fun sendResult(result: String) { val resultIntent = Intent().putExtra(DropIn.RESULT_KEY, result) setResult(Activity.RESULT_OK, resultIntent) @@ -497,30 +514,30 @@ internal class DropInActivity : } private fun terminateSuccessfully() { - Logger.d(TAG, "terminateSuccessfully") + adyenLog(AdyenLogLevel.DEBUG) { "terminateSuccessfully" } terminate() } private fun terminateWithError(reason: String) { - Logger.d(TAG, "terminateWithError") + adyenLog(AdyenLogLevel.DEBUG) { "terminateWithError" } val resultIntent = Intent().putExtra(DropIn.ERROR_REASON_KEY, reason) setResult(Activity.RESULT_CANCELED, resultIntent) terminate() } private fun terminate() { - Logger.d(TAG, "terminate") + adyenLog(AdyenLogLevel.DEBUG) { "terminate" } stopDropInService() finish() overridePendingTransition(0, R.anim.fade_out) } private fun handleIntent(intent: Intent) { - Logger.d(TAG, "handleIntent: action - ${intent.action}") + adyenLog(AdyenLogLevel.DEBUG) { "handleIntent: action - ${intent.action}" } dropInViewModel.isWaitingResult = false if (isWeChatPayIntent(intent)) { - Logger.d(TAG, "isResultIntent") + adyenLog(AdyenLogLevel.DEBUG) { "isResultIntent" } handleActionIntentResponse(intent) } @@ -531,12 +548,12 @@ internal class DropInActivity : if (data != null && data.toString().startsWith(RedirectComponent.REDIRECT_RESULT_SCHEME)) { handleActionIntentResponse(intent) } else { - Logger.e(TAG, "Unexpected response from ACTION_VIEW - ${intent.data}") + adyenLog(AdyenLogLevel.ERROR) { "Unexpected response from ACTION_VIEW - ${intent.data}" } } } else -> { - Logger.e(TAG, "Unable to find action") + adyenLog(AdyenLogLevel.ERROR) { "Unable to find action" } } } } @@ -550,7 +567,7 @@ internal class DropInActivity : private fun getActionFragment(): ActionComponentDialogFragment? { val fragment = getFragmentByTag(ACTION_FRAGMENT_TAG) as? ActionComponentDialogFragment - if (fragment == null) Logger.e(TAG, "ActionComponentDialogFragment is not loaded") + if (fragment == null) adyenLog(AdyenLogLevel.ERROR) { "ActionComponentDialogFragment is not loaded" } return fragment } @@ -616,15 +633,15 @@ internal class DropInActivity : } private fun handleBalanceResult(balanceResult: BalanceResult) { - Logger.v(TAG, "handleBalanceResult") + adyenLog(AdyenLogLevel.VERBOSE) { "handleBalanceResult" } val result = dropInViewModel.handleBalanceResult(balanceResult) - Logger.d(TAG, "handleBalanceResult: ${result::class.java.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "handleBalanceResult: ${result::class.java.simpleName}" } when (result) { is GiftCardBalanceResult.Error -> showError( dialogTitle = null, errorMessage = getString(result.errorMessage), reason = result.reason, - terminate = result.terminateDropIn + terminate = result.terminateDropIn, ) is GiftCardBalanceResult.FullPayment -> handleGiftCardFullPayment(result) @@ -634,20 +651,20 @@ internal class DropInActivity : } private fun handleGiftCardFullPayment(fullPayment: GiftCardBalanceResult.FullPayment) { - Logger.d(TAG, "handleGiftCardFullPayment") + adyenLog(AdyenLogLevel.DEBUG) { "handleGiftCardFullPayment" } setLoading(false) showGiftCardPaymentConfirmationDialog(fullPayment.data) } private fun showGiftCardPaymentConfirmationDialog(data: GiftCardPaymentConfirmationData) { - Logger.d(TAG, "showGiftCardPaymentConfirmationDialog") + adyenLog(AdyenLogLevel.DEBUG) { "showGiftCardPaymentConfirmationDialog" } hideAllScreens() GiftCardPaymentConfirmationDialogFragment.newInstance(data) .show(supportFragmentManager, GIFT_CARD_PAYMENT_CONFIRMATION_FRAGMENT_TAG) } private fun handleOrderResult(order: OrderResponse) { - Logger.v(TAG, "handleOrderResult") + adyenLog(AdyenLogLevel.VERBOSE) { "handleOrderResult" } dropInViewModel.handleOrderCreated(order) } @@ -690,6 +707,14 @@ internal class DropInActivity : dropInService?.onBinLookupCalled(data) } + override fun onAddressLookupQuery(query: String) { + dropInService?.onAddressLookupQueryChangedCalled(query) + } + + override fun onAddressLookupCompletion(lookupAddress: LookupAddress): Boolean { + return dropInService?.onAddressLookupCompletionCalled(lookupAddress) ?: false + } + private fun showDialog(title: String, message: String, onDismiss: () -> Unit) { AlertDialog.Builder(this) .setTitle(title) @@ -700,9 +725,6 @@ internal class DropInActivity : } companion object { - - private val TAG = LogUtil.getTag() - private const val PRESELECTED_PAYMENT_METHOD_FRAGMENT_TAG = "PRESELECTED_PAYMENT_METHOD_FRAGMENT" private const val PAYMENT_METHODS_LIST_FRAGMENT_TAG = "PAYMENT_METHODS_LIST_FRAGMENT" private const val COMPONENT_FRAGMENT_TAG = "COMPONENT_DIALOG_FRAGMENT" @@ -714,14 +736,14 @@ internal class DropInActivity : fun createIntent( context: Context, - dropInConfiguration: DropInConfiguration, + checkoutConfiguration: CheckoutConfiguration, paymentMethodsApiResponse: PaymentMethodsApiResponse, service: ComponentName, ): Intent { val intent = Intent(context, DropInActivity::class.java) DropInBundleHandler.putIntentExtras( intent = intent, - dropInConfiguration = dropInConfiguration, + checkoutConfiguration = checkoutConfiguration, paymentMethodsApiResponse = paymentMethodsApiResponse, service = service, ) @@ -730,14 +752,14 @@ internal class DropInActivity : fun createIntent( context: Context, - dropInConfiguration: DropInConfiguration, + checkoutConfiguration: CheckoutConfiguration, checkoutSession: CheckoutSession, service: ComponentName, ): Intent { val intent = Intent(context, DropInActivity::class.java) DropInBundleHandler.putIntentExtras( intent = intent, - dropInConfiguration = dropInConfiguration, + checkoutConfiguration = checkoutConfiguration, checkoutSession = checkoutSession, service = service, ) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInBottomSheetDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInBottomSheetDialogFragment.kt index e1ce1d29a3..1913fc218d 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInBottomSheetDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInBottomSheetDialogFragment.kt @@ -17,11 +17,12 @@ import android.widget.FrameLayout import androidx.fragment.app.activityViewModels import com.adyen.checkout.card.BinLookupData import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.giftcard.GiftCardComponentState import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog @@ -58,7 +59,7 @@ internal abstract class DropInBottomSheetDialogFragment : BottomSheetDialogFragm dialog.setOnShowListener { val bottomSheet = (dialog as BottomSheetDialog).findViewById( - com.google.android.material.R.id.design_bottom_sheet + com.google.android.material.R.id.design_bottom_sheet, ) if (bottomSheet != null) { @@ -69,7 +70,7 @@ internal abstract class DropInBottomSheetDialogFragment : BottomSheetDialogFragm } behavior.state = this.dialogInitViewState } else { - Logger.e(TAG, "Failed to set BottomSheetBehavior.") + adyenLog(AdyenLogLevel.ERROR) { "Failed to set BottomSheetBehavior." } } } @@ -83,14 +84,10 @@ internal abstract class DropInBottomSheetDialogFragment : BottomSheetDialogFragm } override fun onCancel(dialog: DialogInterface) { - Logger.d(TAG, "onCancel") + adyenLog(AdyenLogLevel.DEBUG) { "onCancel" } protocol.terminateDropIn() } - companion object { - private val TAG = LogUtil.getTag() - } - /** * Interface for Drop-in fragments to interact with the main Activity */ @@ -112,5 +109,7 @@ internal abstract class DropInBottomSheetDialogFragment : BottomSheetDialogFragm fun onRedirect() fun onBinValue(binValue: String) fun onBinLookup(data: List) + fun onAddressLookupQuery(query: String) + fun onAddressLookupCompletion(lookupAddress: LookupAddress): Boolean } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInSavedStateHandleContainer.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInSavedStateHandleContainer.kt index 6dea75420e..5f88a24bb5 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInSavedStateHandleContainer.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInSavedStateHandleContainer.kt @@ -13,12 +13,12 @@ import android.content.Intent import android.os.Bundle import androidx.lifecycle.SavedStateHandle import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.components.core.internal.SavedStateHandleContainer import com.adyen.checkout.components.core.internal.SavedStateHandleProperty -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.internal.ui.model.OrderModel import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.sessions.core.CheckoutSession @@ -29,9 +29,8 @@ internal class DropInSavedStateHandleContainer( override val savedStateHandle: SavedStateHandle, ) : SavedStateHandleContainer { - var dropInConfiguration: DropInConfiguration? by SavedStateHandleProperty(DROP_IN_CONFIGURATION_KEY) + var checkoutConfiguration: CheckoutConfiguration? by SavedStateHandleProperty(CHECKOUT_CONFIGURATION_KEY) var serviceComponentName: ComponentName? by SavedStateHandleProperty(DROP_IN_SERVICE_KEY) - var amount: Amount? by SavedStateHandleProperty(AMOUNT) var sessionDetails: SessionDetails? by SavedStateHandleProperty(SESSION_KEY) var isSessionsFlowTakenOver: Boolean? by SavedStateHandleProperty(IS_SESSIONS_FLOW_TAKEN_OVER_KEY) var paymentMethodsApiResponse: PaymentMethodsApiResponse? by SavedStateHandleProperty(PAYMENT_METHODS_RESPONSE_KEY) @@ -42,50 +41,49 @@ internal class DropInSavedStateHandleContainer( } internal object DropInBundleHandler { - private val TAG = LogUtil.getTag() fun putIntentExtras( intent: Intent, - dropInConfiguration: DropInConfiguration, + checkoutConfiguration: CheckoutConfiguration, paymentMethodsApiResponse: PaymentMethodsApiResponse, service: ComponentName, ) { intent.apply { putExtra(PAYMENT_METHODS_RESPONSE_KEY, paymentMethodsApiResponse) - putExtra(DROP_IN_CONFIGURATION_KEY, dropInConfiguration) + putExtra(CHECKOUT_CONFIGURATION_KEY, checkoutConfiguration) putExtra(DROP_IN_SERVICE_KEY, service) - putExtra(AMOUNT, dropInConfiguration.amount) } } fun putIntentExtras( intent: Intent, - dropInConfiguration: DropInConfiguration, + checkoutConfiguration: CheckoutConfiguration, checkoutSession: CheckoutSession, service: ComponentName, ) { putIntentExtras( intent, - dropInConfiguration, + checkoutConfiguration, checkoutSession.sessionSetupResponse.paymentMethodsApiResponse ?: PaymentMethodsApiResponse(), service, ) intent.apply { - putExtra(SESSION_KEY, checkoutSession.sessionSetupResponse.mapToDetails()) - putExtra(AMOUNT, checkoutSession.sessionSetupResponse.amount) + putExtra(SESSION_KEY, checkoutSession.mapToDetails()) } } fun assertBundleExists(bundle: Bundle?): Boolean { return when { bundle == null -> { - Logger.e(TAG, "Failed to initialize - bundle is null") + adyenLog(AdyenLogLevel.ERROR) { "Failed to initialize - bundle is null" } false } - !bundle.containsKey(DROP_IN_SERVICE_KEY) || !bundle.containsKey(DROP_IN_CONFIGURATION_KEY) -> { - Logger.e(TAG, "Failed to initialize - bundle does not have the required keys") + + !bundle.containsKey(DROP_IN_SERVICE_KEY) || !bundle.containsKey(CHECKOUT_CONFIGURATION_KEY) -> { + adyenLog(AdyenLogLevel.ERROR) { "Failed to initialize - bundle does not have the required keys" } false } + else -> true } } @@ -94,10 +92,9 @@ internal object DropInBundleHandler { private const val PAYMENT_METHODS_RESPONSE_KEY = "PAYMENT_METHODS_RESPONSE_KEY" private const val SESSION_KEY = "SESSION_KEY" private const val IS_SESSIONS_FLOW_TAKEN_OVER_KEY = "IS_SESSIONS_FLOW_TAKEN_OVER_KEY" -private const val DROP_IN_CONFIGURATION_KEY = "DROP_IN_CONFIGURATION_KEY" +private const val CHECKOUT_CONFIGURATION_KEY = "CHECKOUT_CONFIGURATION_KEY" private const val DROP_IN_SERVICE_KEY = "DROP_IN_SERVICE_KEY" private const val IS_WAITING_FOR_RESULT_KEY = "IS_WAITING_FOR_RESULT_KEY" private const val CACHED_GIFT_CARD = "CACHED_GIFT_CARD" private const val CURRENT_ORDER = "CURRENT_ORDER" private const val PARTIAL_PAYMENT_AMOUNT = "PARTIAL_PAYMENT_AMOUNT" -private const val AMOUNT = "AMOUNT" diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModel.kt index 78324ac66c..8ff6142543 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModel.kt @@ -11,8 +11,11 @@ package com.adyen.checkout.dropin.internal.ui import android.content.ComponentName import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.adyen.checkout.components.core.AddressLookupResult import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.BalanceResult +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.PaymentComponentData @@ -23,19 +26,20 @@ import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.data.api.OrderStatusRepository +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.components.core.internal.util.isEmpty import com.adyen.checkout.components.core.paymentmethod.GiftCardPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R -import com.adyen.checkout.dropin.internal.provider.checkCompileOnly import com.adyen.checkout.dropin.internal.ui.model.DropInActivityEvent import com.adyen.checkout.dropin.internal.ui.model.DropInDestination +import com.adyen.checkout.dropin.internal.ui.model.DropInOverrideParamsFactory +import com.adyen.checkout.dropin.internal.ui.model.DropInParams import com.adyen.checkout.dropin.internal.ui.model.GiftCardPaymentConfirmationData import com.adyen.checkout.dropin.internal.ui.model.OrderModel +import com.adyen.checkout.dropin.internal.util.checkCompileOnly import com.adyen.checkout.dropin.internal.util.isStoredPaymentSupported import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.internal.util.GiftCardBalanceStatus @@ -43,8 +47,10 @@ import com.adyen.checkout.giftcard.internal.util.GiftCardBalanceUtils import com.adyen.checkout.googlepay.GooglePayComponent import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails import com.adyen.checkout.sessions.core.internal.data.model.mapToModel +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -53,22 +59,28 @@ internal class DropInViewModel( private val bundleHandler: DropInSavedStateHandleContainer, private val orderStatusRepository: OrderStatusRepository, internal val analyticsRepository: AnalyticsRepository, + private val initialDropInParams: DropInParams, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : ViewModel() { private val eventChannel: Channel = bufferedChannel() internal val eventsFlow = eventChannel.receiveAsFlow() - val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration) + // this should only be used when initializing components, for drop-in related configurations use dropInParams + val checkoutConfiguration: CheckoutConfiguration = requireNotNull(bundleHandler.checkoutConfiguration) - val serviceComponentName: ComponentName = requireNotNull(bundleHandler.serviceComponentName) - - var amount: Amount? - get() = bundleHandler.amount - private set(value) { - bundleHandler.amount = value + val dropInParams: DropInParams + get() { + if (overrideAmount == null) return initialDropInParams + return initialDropInParams.copy(amount = overrideAmount) } - internal var sessionDetails: SessionDetails? + // this is needed for partial payments, the amount has to be updated manually to override the initial amount + private var overrideAmount: Amount? = null + + val serviceComponentName: ComponentName = requireNotNull(bundleHandler.serviceComponentName) + + private var sessionDetails: SessionDetails? get() = bundleHandler.sessionDetails private set(value) { bundleHandler.sessionDetails = value @@ -110,6 +122,12 @@ internal class DropInViewModel( bundleHandler.currentOrder = value } + private val _addressLookupOptionsFlow = bufferedChannel>() + val addressLookupOptionsFlow: Flow> = _addressLookupOptionsFlow.receiveAsFlow() + + private val _addressLookupCompleteFlow = bufferedChannel() + val addressLookupCompleteFlow: Flow = _addressLookupCompleteFlow.receiveAsFlow() + fun getPaymentMethods(): List { return paymentMethodsApiResponse.paymentMethods.orEmpty() } @@ -120,7 +138,7 @@ internal class DropInViewModel( fun shouldShowPreselectedStored(): Boolean { return getStoredPaymentMethods().any { it.isStoredPaymentSupported() } && - dropInConfiguration.showPreselectedStoredPaymentMethod + dropInParams.showPreselectedStoredPaymentMethod } fun getPreselectedStoredPaymentMethod(): StoredPaymentMethod { @@ -134,7 +152,7 @@ internal class DropInViewModel( } fun shouldSkipToSinglePaymentMethod(): Boolean { - if (!dropInConfiguration.skipListWhenSinglePaymentMethod) return false + if (!dropInParams.skipListWhenSinglePaymentMethod) return false val noStored = getStoredPaymentMethods().isEmpty() val singlePm = getPaymentMethods().size == 1 @@ -151,8 +169,19 @@ internal class DropInViewModel( return noStored && singlePm && paymentMethodHasComponent } - private fun getInitialAmount(): Amount? { - return sessionDetails?.amount ?: dropInConfiguration.amount + /** + * @return A class needed to initialize components inside drop-in. + */ + fun getDropInOverrideParams(): DropInOverrideParams { + val amount = dropInParams.amount + return DropInOverrideParamsFactory.create( + amount = amount, + // when creating a component, the sessions amount has priority over the drop-in amount + // but after a partial payment is successful the sessions amount is not updated to the remaining amount + // this is due to the backend not returning the updated amount value in the sessions setup call + // therefore we modify the amount ourselves here before passing it to the components + sessionDetails = sessionDetails?.copy(amount = amount), + ) } fun onCreated() { @@ -163,15 +192,15 @@ internal class DropInViewModel( fun onDropInServiceConnected() { val sessionModel = sessionDetails?.mapToModel() if (sessionModel == null) { - Logger.d(TAG, "Session is null") + adyenLog(AdyenLogLevel.DEBUG) { "Session is null" } return } val event = DropInActivityEvent.SessionServiceConnected( sessionModel = sessionModel, - clientKey = dropInConfiguration.clientKey, - environment = dropInConfiguration.environment, - isFlowTakenOver = isSessionsFlowTakenOver + clientKey = dropInParams.clientKey, + environment = dropInParams.environment, + isFlowTakenOver = isSessionsFlowTakenOver, ) sendEvent(event) } @@ -202,7 +231,7 @@ internal class DropInViewModel( } private fun setupAnalytics() { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } viewModelScope.launch { analyticsRepository.setupAnalytics() } @@ -216,7 +245,7 @@ internal class DropInViewModel( ): PaymentComponentData? { val paymentMethod = giftCardComponentState.data.paymentMethod if (paymentMethod == null) { - Logger.e(TAG, "onBalanceCallRequested - paymentMethod is null") + adyenLog(AdyenLogLevel.ERROR) { "onBalanceCallRequested - paymentMethod is null" } return null } cachedGiftCardComponentState = giftCardComponentState @@ -224,51 +253,49 @@ internal class DropInViewModel( } fun handleBalanceResult(balanceResult: BalanceResult): GiftCardBalanceResult { - Logger.d( - TAG, + adyenLog(AdyenLogLevel.DEBUG) { "handleBalanceResult - balance: ${balanceResult.balance} - " + "transactionLimit: ${balanceResult.transactionLimit}" - ) + } val giftCardBalanceResult = GiftCardBalanceUtils.checkBalance( balance = balanceResult.balance, transactionLimit = balanceResult.transactionLimit, - amountToBePaid = amount + amountToBePaid = dropInParams.amount, ) val cachedGiftCardComponentState = cachedGiftCardComponentState ?: throw CheckoutException("Failed to retrieved cached gift card object") return when (giftCardBalanceResult) { is GiftCardBalanceStatus.ZeroBalance -> { - Logger.i(TAG, "handleBalanceResult - Gift Card has zero balance") + adyenLog(AdyenLogLevel.INFO) { "handleBalanceResult - Gift Card has zero balance" } GiftCardBalanceResult.Error( R.string.checkout_giftcard_error_zero_balance, "Gift Card has zero balance", - false + false, ) } is GiftCardBalanceStatus.NonMatchingCurrencies -> { - Logger.e(TAG, "handleBalanceResult - Gift Card currency mismatch") + adyenLog(AdyenLogLevel.ERROR) { "handleBalanceResult - Gift Card currency mismatch" } GiftCardBalanceResult.Error( R.string.checkout_giftcard_error_currency, "Gift Card currency mismatch", - false + false, ) } is GiftCardBalanceStatus.ZeroAmountToBePaid -> { - Logger.e( - TAG, + adyenLog(AdyenLogLevel.ERROR) { "handleBalanceResult - You must set an amount in DropInConfiguration.Builder to enable gift " + "card payments" - ) + } GiftCardBalanceResult.Error(R.string.payment_failed, "Drop-in amount is not set", true) } is GiftCardBalanceStatus.FullPayment -> { cachedPartialPaymentAmount = giftCardBalanceResult.amountPaid GiftCardBalanceResult.FullPayment( - createGiftCardPaymentConfirmationData(giftCardBalanceResult, cachedGiftCardComponentState) + createGiftCardPaymentConfirmationData(giftCardBalanceResult, cachedGiftCardComponentState), ) } @@ -290,14 +317,14 @@ internal class DropInViewModel( return GiftCardPaymentConfirmationData( amountPaid = giftCardBalanceStatus.amountPaid, remainingBalance = giftCardBalanceStatus.remainingBalance, - shopperLocale = dropInConfiguration.shopperLocale, + shopperLocale = dropInParams.shopperLocale, brand = giftCardComponentState.data.paymentMethod?.brand.orEmpty(), - lastFourDigits = giftCardComponentState.lastFourDigits.orEmpty() + lastFourDigits = giftCardComponentState.lastFourDigits.orEmpty(), ) } fun handleOrderCreated(orderResponse: OrderResponse) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(coroutineDispatcher) { handleOrderResponse(orderResponse) if (currentOrder != null) { partialPaymentRequested() @@ -309,15 +336,14 @@ internal class DropInViewModel( val orderModel = getOrderDetails(orderResponse) if (orderModel == null) { currentOrder = null - amount = getInitialAmount() - Logger.d(TAG, "handleOrderResponse - Amount reverted: $amount") - Logger.d(TAG, "handleOrderResponse - Order cancelled") + overrideAmount = null + adyenLog(AdyenLogLevel.DEBUG) { "handleOrderResponse - Amount reverted: ${dropInParams.amount}" } + adyenLog(AdyenLogLevel.DEBUG) { "handleOrderResponse - Order cancelled" } } else { currentOrder = orderModel - amount = orderModel.remainingAmount - sessionDetails = sessionDetails?.copy(amount = orderModel.remainingAmount) - Logger.d(TAG, "handleOrderResponse - New amount set: $amount") - Logger.d(TAG, "handleOrderResponse - Order cached") + overrideAmount = orderModel.remainingAmount + adyenLog(AdyenLogLevel.DEBUG) { "handleOrderResponse - New amount set: ${dropInParams.amount}" } + adyenLog(AdyenLogLevel.DEBUG) { "handleOrderResponse - Order cached" } } } @@ -326,33 +352,33 @@ internal class DropInViewModel( val existingAmount = paymentComponentState.data.amount when { existingAmount != null -> { - Logger.d(TAG, "Payment amount already set: $existingAmount") + adyenLog(AdyenLogLevel.DEBUG) { "Payment amount already set: $existingAmount" } } - amount != null -> { - paymentComponentState.data.amount = amount - Logger.d(TAG, "Payment amount set: $amount") + dropInParams.amount != null -> { + paymentComponentState.data.amount = dropInParams.amount + adyenLog(AdyenLogLevel.DEBUG) { "Payment amount set: ${dropInParams.amount}" } } else -> { - Logger.d(TAG, "Payment amount not set") + adyenLog(AdyenLogLevel.DEBUG) { "Payment amount not set" } } } currentOrder?.let { paymentComponentState.data.order = createOrder(it) - Logger.d(TAG, "Order appended to payment") + adyenLog(AdyenLogLevel.DEBUG) { "Order appended to payment" } } } private fun createOrder(order: OrderModel): OrderRequest { return OrderRequest( pspReference = order.pspReference, - orderData = order.orderData + orderData = order.orderData, ) } fun handlePaymentMethodsUpdate(paymentMethodsApiResponse: PaymentMethodsApiResponse, order: OrderResponse?) { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(coroutineDispatcher) { handleOrderResponse(order) this@DropInViewModel.paymentMethodsApiResponse = paymentMethodsApiResponse sendEvent(DropInActivityEvent.ShowPaymentMethods) @@ -378,20 +404,20 @@ internal class DropInViewModel( private suspend fun getOrderDetails(orderResponse: OrderResponse?): OrderModel? { if (orderResponse == null) return null - return orderStatusRepository.getOrderStatus(dropInConfiguration, orderResponse.orderData) + return orderStatusRepository.getOrderStatus(dropInParams.clientKey, orderResponse.orderData) .fold( onSuccess = { statusResponse -> OrderModel( orderData = orderResponse.orderData, pspReference = orderResponse.pspReference, remainingAmount = statusResponse.remainingAmount, - paymentMethods = statusResponse.paymentMethods + paymentMethods = statusResponse.paymentMethods, ) }, onFailure = { e -> - Logger.e(TAG, "Unable to fetch order details", e) + adyenLog(AdyenLogLevel.ERROR, e) { "Unable to fetch order details" } null - } + }, ) } @@ -401,7 +427,7 @@ internal class DropInViewModel( val partialPaymentAmount = cachedPartialPaymentAmount ?: throw CheckoutException("Lost reference to cached partial payment amount") paymentComponentState.data.amount = partialPaymentAmount - Logger.d(TAG, "Partial payment amount set: $partialPaymentAmount") + adyenLog(AdyenLogLevel.DEBUG) { "Partial payment amount set: $partialPaymentAmount" } cachedGiftCardComponentState = null cachedPartialPaymentAmount = null sendEvent(DropInActivityEvent.MakePartialPayment(paymentComponentState)) @@ -413,6 +439,16 @@ internal class DropInViewModel( sendCancelOrderEvent(order, false) } + fun onAddressLookupOptions(options: List) { + adyenLog(AdyenLogLevel.DEBUG) { "onAddressLookupOptions $options" } + viewModelScope.launch { _addressLookupOptionsFlow.send(options) } + } + + fun onAddressLookupComplete(lookupAddress: LookupAddress) { + adyenLog(AdyenLogLevel.DEBUG) { "onAddressLookupComplete $lookupAddress" } + viewModelScope.launch { _addressLookupCompleteFlow.send(AddressLookupResult.Completed(lookupAddress)) } + } + fun cancelDropIn() { currentOrder?.let { sendCancelOrderEvent(it, true) } sendEvent(DropInActivityEvent.CancelDropIn) @@ -421,19 +457,15 @@ internal class DropInViewModel( private fun sendCancelOrderEvent(order: OrderModel, isDropInCancelledByUser: Boolean) { val orderRequest = OrderRequest( pspReference = order.pspReference, - orderData = order.orderData + orderData = order.orderData, ) sendEvent(DropInActivityEvent.CancelOrder(orderRequest, isDropInCancelledByUser)) } private fun sendEvent(event: DropInActivityEvent) { viewModelScope.launch { - Logger.d(TAG, "sendEvent - ${event::class.simpleName}") + adyenLog(AdyenLogLevel.DEBUG) { "sendEvent - ${event::class.simpleName}" } eventChannel.send(event) } } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt index 0faff2ce20..a3e87505b4 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/DropInViewModelFactory.kt @@ -12,7 +12,7 @@ import androidx.activity.ComponentActivity import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryData import com.adyen.checkout.components.core.internal.data.api.AnalyticsService @@ -20,52 +20,72 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepo import com.adyen.checkout.components.core.internal.data.api.OrderStatusRepository import com.adyen.checkout.components.core.internal.data.api.OrderStatusService import com.adyen.checkout.components.core.internal.data.model.AnalyticsSource -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.util.screenWidthPixels import com.adyen.checkout.core.internal.data.api.HttpClientFactory -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.core.internal.util.LocaleProvider +import com.adyen.checkout.dropin.internal.ui.model.DropInParams +import com.adyen.checkout.dropin.internal.ui.model.DropInParamsMapper import com.adyen.checkout.dropin.internal.ui.model.DropInPaymentMethodInformation import com.adyen.checkout.dropin.internal.ui.model.overrideInformation +import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails +import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory +import java.util.Locale internal class DropInViewModelFactory( - activity: ComponentActivity + activity: ComponentActivity, + localeProvider: LocaleProvider = LocaleProvider(), ) : AbstractSavedStateViewModelFactory(activity, activity.intent.extras) { private val packageName: String = activity.packageName private val screenWidth: Int = activity.screenWidthPixels + private val deviceLocale: Locale = localeProvider.getLocale(activity) override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { val bundleHandler = DropInSavedStateHandleContainer(handle) - val dropInConfiguration: DropInConfiguration = requireNotNull(bundleHandler.dropInConfiguration) - bundleHandler.overridePaymentMethodInformation(dropInConfiguration.overriddenPaymentMethodInformation) + val dropInParams = getDropInParams( + checkoutConfiguration = requireNotNull(bundleHandler.checkoutConfiguration), + sessionDetails = bundleHandler.sessionDetails, + ) + + bundleHandler.overridePaymentMethodInformation(dropInParams.overriddenPaymentMethodInformation) - val amount: Amount? = bundleHandler.amount val paymentMethods = bundleHandler.paymentMethodsApiResponse?.paymentMethods?.mapNotNull { it.type }.orEmpty() - val session = bundleHandler.sessionDetails - val httpClient = HttpClientFactory.getHttpClient(dropInConfiguration.environment) + val httpClient = HttpClientFactory.getHttpClient(dropInParams.environment) val orderStatusRepository = OrderStatusRepository(OrderStatusService(httpClient)) val analyticsRepository = DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( - level = AnalyticsParams(dropInConfiguration.analyticsConfiguration).level, + level = dropInParams.analyticsParams.level, packageName = packageName, - locale = dropInConfiguration.shopperLocale, + locale = dropInParams.shopperLocale, source = AnalyticsSource.DropIn(), - clientKey = dropInConfiguration.clientKey, - amount = amount, + clientKey = dropInParams.clientKey, + amount = dropInParams.amount, screenWidth = screenWidth, paymentMethods = paymentMethods, - sessionId = session?.id, + sessionId = bundleHandler.sessionDetails?.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(dropInConfiguration.environment) + httpClient = HttpClientFactory.getAnalyticsHttpClient(dropInParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @Suppress("UNCHECKED_CAST") - return DropInViewModel(bundleHandler, orderStatusRepository, analyticsRepository) as T + return DropInViewModel(bundleHandler, orderStatusRepository, analyticsRepository, dropInParams) as T + } + + private fun getDropInParams( + checkoutConfiguration: CheckoutConfiguration, + sessionDetails: SessionDetails? + ): DropInParams { + val sessionParams = sessionDetails?.let { SessionParamsFactory.create(sessionDetails) } + return DropInParamsMapper().mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = deviceLocale, + sessionParams = sessionParams, + ) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GenericComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GenericComponentDialogFragment.kt index bf9096881a..5f96894bea 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GenericComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GenericComponentDialogFragment.kt @@ -15,9 +15,9 @@ import android.view.ViewGroup import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.internal.ButtonComponent import com.adyen.checkout.components.core.internal.PaymentComponent +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.databinding.FragmentGenericComponentBinding import com.adyen.checkout.ui.core.internal.ui.ViewableComponent import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -34,7 +34,7 @@ internal class GenericComponentDialogFragment : BaseComponentDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } binding.header.text = paymentMethod.name try { @@ -60,7 +60,5 @@ internal class GenericComponentDialogFragment : BaseComponentDialogFragment() { super.onDestroyView() } - companion object : BaseCompanion(GenericComponentDialogFragment::class.java) { - private val TAG = LogUtil.getTag() - } + companion object : BaseCompanion(GenericComponentDialogFragment::class.java) } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardComponentDialogFragment.kt index 3b6af93bb2..0bd45ebe6d 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardComponentDialogFragment.kt @@ -17,9 +17,9 @@ import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentComponent +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.databinding.FragmentGiftcardComponentBinding import com.adyen.checkout.dropin.internal.provider.getComponentFor @@ -39,7 +39,7 @@ internal class GiftCardComponentDialogFragment : DropInBottomSheetDialogFragment private lateinit var giftCardComponent: GiftCardComponent override fun onCreate(savedInstanceState: Bundle?) { - Logger.d(TAG, "onCreate") + adyenLog(AdyenLogLevel.DEBUG) { "onCreate" } super.onCreate(savedInstanceState) arguments?.let { paymentMethod = it.getParcelable(PAYMENT_METHOD) @@ -53,7 +53,7 @@ internal class GiftCardComponentDialogFragment : DropInBottomSheetDialogFragment } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } binding.header.text = paymentMethod.name try { @@ -70,10 +70,9 @@ internal class GiftCardComponentDialogFragment : DropInBottomSheetDialogFragment giftCardComponent = getComponentFor( fragment = this, paymentMethod = paymentMethod, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), componentCallback = this, - sessionDetails = dropInViewModel.sessionDetails, analyticsRepository = dropInViewModel.analyticsRepository, onRedirect = protocol::onRedirect, ) as GiftCardComponent @@ -97,7 +96,7 @@ internal class GiftCardComponentDialogFragment : DropInBottomSheetDialogFragment } override fun onRequestOrder() { - Logger.d(TAG, "onRequestOrder") + adyenLog(AdyenLogLevel.DEBUG) { "onRequestOrder" } // no ops } @@ -105,27 +104,27 @@ internal class GiftCardComponentDialogFragment : DropInBottomSheetDialogFragment if (paymentComponentState !is GiftCardComponentState) { throw CheckoutException("paymentComponentState is not an instance of GiftCardComponentState.") } - Logger.d(TAG, "onBalanceCheck") + adyenLog(AdyenLogLevel.DEBUG) { "onBalanceCheck" } protocol.requestBalanceCall(paymentComponentState) } override fun onSubmit(state: GiftCardComponentState) { - Logger.d(TAG, "onSubmit") + adyenLog(AdyenLogLevel.DEBUG) { "onSubmit" } // no ops } override fun onAdditionalDetails(actionComponentData: ActionComponentData) { - Logger.d(TAG, "onAdditionalDetails") + adyenLog(AdyenLogLevel.DEBUG) { "onAdditionalDetails" } // no ops } override fun onError(componentError: ComponentError) { - Logger.d(TAG, "onError") + adyenLog(AdyenLogLevel.DEBUG) { "onError" } handleError(componentError) } private fun handleError(componentError: ComponentError) { - Logger.e(TAG, componentError.errorMessage) + adyenLog(AdyenLogLevel.ERROR) { componentError.errorMessage } protocol.showError(null, getString(R.string.component_error), componentError.errorMessage, true) } @@ -147,8 +146,6 @@ internal class GiftCardComponentDialogFragment : DropInBottomSheetDialogFragment } companion object { - private val TAG = LogUtil.getTag() - private const val PAYMENT_METHOD = "PAYMENT_METHOD" fun newInstance( diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardPaymentConfirmationDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardPaymentConfirmationDialogFragment.kt index 87d0bcb6b2..689267562e 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardPaymentConfirmationDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GiftCardPaymentConfirmationDialogFragment.kt @@ -14,8 +14,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.adyen.checkout.components.core.internal.util.CurrencyUtils -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.databinding.FragmentGiftCardPaymentConfirmationBinding import com.adyen.checkout.dropin.internal.ui.model.GiftCardPaymentConfirmationData @@ -36,11 +36,11 @@ internal class GiftCardPaymentConfirmationDialogFragment : DropInBottomSheetDial override fun onAttach(context: Context) { super.onAttach(context) - Logger.d(TAG, "onAttach") + adyenLog(AdyenLogLevel.DEBUG) { "onAttach" } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - Logger.d(TAG, "onCreateView") + adyenLog(AdyenLogLevel.DEBUG) { "onCreateView" } _binding = FragmentGiftCardPaymentConfirmationBinding.inflate(inflater, container, false) return binding.root } @@ -50,13 +50,13 @@ internal class GiftCardPaymentConfirmationDialogFragment : DropInBottomSheetDial val amountToPay = CurrencyUtils.formatAmount( giftCardPaymentConfirmationData.amountPaid, - giftCardPaymentConfirmationData.shopperLocale + giftCardPaymentConfirmationData.shopperLocale, ) binding.payButton.text = String.format(resources.getString(R.string.pay_button_with_value), amountToPay) val remainingBalance = CurrencyUtils.formatAmount( giftCardPaymentConfirmationData.remainingBalance, - giftCardPaymentConfirmationData.shopperLocale + giftCardPaymentConfirmationData.shopperLocale, ) binding.textViewRemainingBalance.text = String.format(resources.getString(R.string.checkout_giftcard_remaining_balance_text), remainingBalance) @@ -80,7 +80,7 @@ internal class GiftCardPaymentConfirmationDialogFragment : DropInBottomSheetDial amount = it.amount, transactionLimit = it.transactionLimit, shopperLocale = giftCardPaymentConfirmationData.shopperLocale, - environment = dropInViewModel.dropInConfiguration.environment, + environment = dropInViewModel.dropInParams.environment, ) } val currentPaymentMethod = GiftCardPaymentMethodModel( @@ -89,7 +89,7 @@ internal class GiftCardPaymentConfirmationDialogFragment : DropInBottomSheetDial amount = null, transactionLimit = null, shopperLocale = null, - environment = dropInViewModel.dropInConfiguration.environment, + environment = dropInViewModel.dropInParams.environment, ) val paymentMethods = alreadyPaidMethods + currentPaymentMethod @@ -119,7 +119,6 @@ internal class GiftCardPaymentConfirmationDialogFragment : DropInBottomSheetDial } companion object { - private val TAG = LogUtil.getTag() private const val GIFT_CARD_DATA = "GIFT_CARD_DATA" @JvmStatic diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayComponentDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayComponentDialogFragment.kt index ce94336c0d..1c3b7fbee7 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayComponentDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayComponentDialogFragment.kt @@ -21,9 +21,9 @@ import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.internal.provider.getComponentFor import com.adyen.checkout.googlepay.GooglePayComponent @@ -41,7 +41,7 @@ internal class GooglePayComponentDialogFragment : private lateinit var component: GooglePayComponent override fun onCreate(savedInstanceState: Bundle?) { - Logger.d(TAG, "onCreate") + adyenLog(AdyenLogLevel.DEBUG) { "onCreate" } super.onCreate(savedInstanceState) arguments?.let { paymentMethod = it.getParcelable(PAYMENT_METHOD) ?: throw IllegalArgumentException("Payment method is null") @@ -51,12 +51,12 @@ internal class GooglePayComponentDialogFragment : } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - Logger.d(TAG, "onCreateView") + adyenLog(AdyenLogLevel.DEBUG) { "onCreateView" } return inflater.inflate(R.layout.fragment_google_pay_component, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -73,10 +73,9 @@ internal class GooglePayComponentDialogFragment : component = getComponentFor( fragment = this, paymentMethod = paymentMethod, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), componentCallback = this, - sessionDetails = dropInViewModel.sessionDetails, analyticsRepository = dropInViewModel.analyticsRepository, onRedirect = protocol::onRedirect, ) as GooglePayComponent @@ -109,12 +108,12 @@ internal class GooglePayComponentDialogFragment : } override fun onBackPressed(): Boolean { - Logger.d(TAG, "onBackPressed") + adyenLog(AdyenLogLevel.DEBUG) { "onBackPressed" } return performBackAction() } private fun handleError(componentError: ComponentError) { - Logger.e(TAG, componentError.errorMessage) + adyenLog(AdyenLogLevel.ERROR) { componentError.errorMessage } // TODO find a way to show an error dialog unless the payment is cancelled by the user // then move back to the payment methods screen afterwards performBackAction() @@ -133,8 +132,6 @@ internal class GooglePayComponentDialogFragment : } companion object { - - private val TAG = LogUtil.getTag() private const val PAYMENT_METHOD = "PAYMENT_METHOD" fun newInstance( diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayViewModel.kt index 81b2224557..49e3654a28 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/GooglePayViewModel.kt @@ -14,8 +14,8 @@ import androidx.lifecycle.viewModelScope import com.adyen.checkout.components.core.internal.SavedStateHandleContainer import com.adyen.checkout.components.core.internal.SavedStateHandleProperty import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -23,10 +23,6 @@ import kotlinx.coroutines.launch internal class GooglePayViewModel( override val savedStateHandle: SavedStateHandle ) : ViewModel(), SavedStateHandleContainer { - companion object { - private val TAG = LogUtil.getTag() - private const val IS_GOOGLE_PAY_STARTED = "IS_GOOGLE_PAY_STARTED" - } private val eventChannel: Channel = bufferedChannel() internal val eventsFlow = eventChannel.receiveAsFlow() @@ -37,10 +33,14 @@ internal class GooglePayViewModel( if (isGooglePayStarted == true) return isGooglePayStarted = true viewModelScope.launch { - Logger.d(TAG, "Sending start GooglePay event") + adyenLog(AdyenLogLevel.DEBUG) { "Sending start GooglePay event" } eventChannel.send(GooglePayFragmentEvent.StartGooglePay) } } + + companion object { + private const val IS_GOOGLE_PAY_STARTED = "IS_GOOGLE_PAY_STARTED" + } } internal sealed class GooglePayFragmentEvent { diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodListDialogFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodListDialogFragment.kt index de7ce56339..a8d19604a0 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodListDialogFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodListDialogFragment.kt @@ -20,8 +20,8 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.PaymentComponent -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.databinding.FragmentPaymentMethodsListBinding import com.adyen.checkout.dropin.internal.provider.getComponentFor @@ -37,8 +37,6 @@ import com.adyen.checkout.ui.core.internal.util.PayButtonFormatter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -private val TAG = LogUtil.getTag() - @Suppress("TooManyFunctions") internal class PaymentMethodListDialogFragment : DropInBottomSheetDialogFragment(), @@ -55,20 +53,20 @@ internal class PaymentMethodListDialogFragment : override fun onAttach(context: Context) { super.onAttach(context) - Logger.d(TAG, "onAttach") + adyenLog(AdyenLogLevel.DEBUG) { "onAttach" } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - Logger.d(TAG, "onCreateView") + adyenLog(AdyenLogLevel.DEBUG) { "onCreateView" } paymentMethodsListViewModel = getViewModel { PaymentMethodsListViewModel( application = requireActivity().application, paymentMethods = dropInViewModel.getPaymentMethods(), storedPaymentMethods = dropInViewModel.getStoredPaymentMethods(), order = dropInViewModel.currentOrder, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, - sessionDetails = dropInViewModel.sessionDetails, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInParams = dropInViewModel.dropInParams, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), ) } _binding = FragmentPaymentMethodsListBinding.inflate(inflater, container, false) @@ -77,7 +75,7 @@ internal class PaymentMethodListDialogFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } initPaymentMethodsRecyclerView() initObservers() @@ -96,7 +94,7 @@ internal class PaymentMethodListDialogFragment : .paymentMethodsFlow .flowWithLifecycle(viewLifecycleOwner.lifecycle) .onEach { paymentMethods -> - Logger.d(TAG, "paymentMethods changed") + adyenLog(AdyenLogLevel.DEBUG) { "paymentMethods changed" } paymentMethodAdapter?.submitList(paymentMethods) }.launchIn(lifecycleScope) @@ -118,7 +116,7 @@ internal class PaymentMethodListDialogFragment : } is PaymentMethodListStoredEvent.ShowError -> { - Logger.e(TAG, event.componentError.errorMessage) + adyenLog(AdyenLogLevel.ERROR) { event.componentError.errorMessage } protocol.showError( dialogTitle = null, errorMessage = getString(R.string.component_error), @@ -152,15 +150,14 @@ internal class PaymentMethodListDialogFragment : } override fun onStoredPaymentMethodSelected(storedPaymentMethodModel: StoredPaymentMethodModel) { - Logger.d(TAG, "onStoredPaymentMethodSelected") + adyenLog(AdyenLogLevel.DEBUG) { "onStoredPaymentMethodSelected" } val storedPaymentMethod = dropInViewModel.getStoredPaymentMethod(storedPaymentMethodModel.id) component = getComponentFor( fragment = this, storedPaymentMethod = storedPaymentMethod, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), componentCallback = paymentMethodsListViewModel, - sessionDetails = dropInViewModel.sessionDetails, analyticsRepository = dropInViewModel.analyticsRepository, onRedirect = protocol::onRedirect, ) @@ -172,18 +169,18 @@ internal class PaymentMethodListDialogFragment : .setTitle( String.format( resources.getString(R.string.checkout_stored_payment_confirmation_message), - paymentMethodName - ) + paymentMethodName, + ), ) .setNegativeButton(R.string.checkout_stored_payment_confirmation_cancel_button) { dialog, _ -> dialog.dismiss() } .setPositiveButton( PayButtonFormatter.getPayButtonText( - amount = dropInViewModel.amount, - locale = dropInViewModel.dropInConfiguration.shopperLocale, + amount = dropInViewModel.dropInParams.amount, + locale = dropInViewModel.dropInParams.shopperLocale, localizedContext = requireContext(), - ) + ), ) { dialog, _ -> dialog.dismiss() paymentMethodsListViewModel.onClickConfirmationButton() @@ -194,8 +191,8 @@ internal class PaymentMethodListDialogFragment : dialog.setMessage( requireActivity().getString( R.string.last_four_digits_format, - storedPaymentMethodModel.lastFour - ) + storedPaymentMethodModel.lastFour, + ), ) } @@ -207,8 +204,8 @@ internal class PaymentMethodListDialogFragment : dialog.setMessage( requireActivity().getString( R.string.last_four_digits_format, - storedPaymentMethodModel.lastFour - ) + storedPaymentMethodModel.lastFour, + ), ) } } @@ -217,7 +214,7 @@ internal class PaymentMethodListDialogFragment : } override fun onPaymentMethodSelected(paymentMethod: PaymentMethodModel) { - Logger.d(TAG, "onPaymentMethodSelected - ${paymentMethod.type}") + adyenLog(AdyenLogLevel.DEBUG) { "onPaymentMethodSelected - ${paymentMethod.type}" } protocol.showComponentDialog(paymentMethodsListViewModel.getPaymentMethod(paymentMethod)) } @@ -230,7 +227,7 @@ internal class PaymentMethodListDialogFragment : override fun onStoredPaymentMethodRemoved(storedPaymentMethodModel: StoredPaymentMethodModel) { val storedPaymentMethod = StoredPaymentMethod( - id = storedPaymentMethodModel.id + id = storedPaymentMethodModel.id, ) protocol.removeStoredPaymentMethod(storedPaymentMethod) } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModel.kt index 7c3a9fd8ac..ded22cc20f 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModel.kt @@ -11,7 +11,7 @@ package com.adyen.checkout.dropin.internal.ui import android.app.Application import androidx.lifecycle.ViewModel import com.adyen.checkout.components.core.ActionComponentData -import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.ComponentError @@ -20,13 +20,14 @@ import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.data.model.OrderPaymentMethod +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.CurrencyUtils import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.internal.provider.checkPaymentMethodAvailability +import com.adyen.checkout.dropin.internal.ui.model.DropInParams import com.adyen.checkout.dropin.internal.ui.model.GiftCardPaymentMethodModel import com.adyen.checkout.dropin.internal.ui.model.OrderModel import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodHeader @@ -36,7 +37,6 @@ import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodNote import com.adyen.checkout.dropin.internal.ui.model.StoredPaymentMethodModel import com.adyen.checkout.dropin.internal.util.isStoredPaymentSupported import com.adyen.checkout.dropin.internal.util.mapStoredModel -import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -49,9 +49,9 @@ internal class PaymentMethodsListViewModel( private val paymentMethods: List, storedPaymentMethods: List, private val order: OrderModel?, - private val dropInConfiguration: DropInConfiguration, - private val amount: Amount?, - private val sessionDetails: SessionDetails?, + private val checkoutConfiguration: CheckoutConfiguration, + private val dropInParams: DropInParams, + private val dropInOverrideParams: DropInOverrideParams, ) : ViewModel(), ComponentAvailableCallback, ComponentCallback> { private val _paymentMethodsFlow = MutableStateFlow>(emptyList()) @@ -80,22 +80,23 @@ internal class PaymentMethodsListViewModel( when { PaymentMethodTypes.SUPPORTED_PAYMENT_METHODS.contains(type) -> { - Logger.d(TAG, "Supported payment method: $type") + adyenLog(AdyenLogLevel.DEBUG) { "Supported payment method: $type" } checkPaymentMethodAvailability( application = application, paymentMethod = paymentMethod, - dropInConfiguration = dropInConfiguration, - amount = amount, - sessionDetails = sessionDetails, - callback = this + checkoutConfiguration = checkoutConfiguration, + dropInOverrideParams = dropInOverrideParams, + callback = this, ) } + PaymentMethodTypes.UNSUPPORTED_PAYMENT_METHODS.contains(type) -> { - Logger.e(TAG, "PaymentMethod not yet supported - $type") + adyenLog(AdyenLogLevel.ERROR) { "PaymentMethod not yet supported - $type" } paymentMethodsAvailabilityMap[paymentMethod] = false } + else -> { - Logger.d(TAG, "No availability check required - $type") + adyenLog(AdyenLogLevel.DEBUG) { "No availability check required - $type" } paymentMethodsAvailabilityMap[paymentMethod] = true } } @@ -104,7 +105,7 @@ internal class PaymentMethodsListViewModel( } override fun onAvailabilityResult(isAvailable: Boolean, paymentMethod: PaymentMethod) { - Logger.d(TAG, "onAvailabilityResult - ${paymentMethod.type}: $isAvailable") + adyenLog(AdyenLogLevel.DEBUG) { "onAvailabilityResult - ${paymentMethod.type}: $isAvailable" } paymentMethodsAvailabilityMap[paymentMethod] = isAvailable checkIfListReady() } @@ -126,9 +127,9 @@ internal class PaymentMethodsListViewModel( } // payment notes order?.remainingAmount?.let { remainingAmount -> - val value = CurrencyUtils.formatAmount(remainingAmount, dropInConfiguration.shopperLocale) + val value = CurrencyUtils.formatAmount(remainingAmount, dropInParams.shopperLocale) add( - PaymentMethodNote(application.getString(R.string.checkout_giftcard_pay_remaining_amount, value)) + PaymentMethodNote(application.getString(R.string.checkout_giftcard_pay_remaining_amount, value)), ) } // stored payment methods @@ -184,8 +185,8 @@ internal class PaymentMethodsListViewModel( eventsChannel.trySend( PaymentMethodListStoredEvent.ShowConfirmationPopup( storedPaymentMethod.name ?: "", - storedPaymentMethodModel - ) + storedPaymentMethodModel, + ), ) } else { eventsChannel.trySend(PaymentMethodListStoredEvent.ShowStoredComponentDialog(storedPaymentMethod)) @@ -211,8 +212,8 @@ internal class PaymentMethodsListViewModel( mapNotNull { storedPaymentMethod -> if (storedPaymentMethod.isStoredPaymentSupported()) { storedPaymentMethod.mapStoredModel( - dropInConfiguration.isRemovingStoredPaymentMethodsEnabled, - dropInConfiguration.environment, + dropInParams.isRemovingStoredPaymentMethodsEnabled, + dropInParams.environment, ) } else { null @@ -233,7 +234,7 @@ internal class PaymentMethodsListViewModel( name = name.orEmpty(), icon = icon.orEmpty(), drawIconBorder = drawIconBorder, - environment = dropInConfiguration.environment, + environment = dropInParams.environment, ) } @@ -244,14 +245,12 @@ internal class PaymentMethodsListViewModel( lastFour = it.lastFour, amount = it.amount, transactionLimit = it.transactionLimit, - shopperLocale = dropInConfiguration.shopperLocale, - environment = dropInConfiguration.environment, + shopperLocale = dropInParams.shopperLocale, + environment = dropInParams.environment, ) } companion object { - private val TAG = LogUtil.getTag() - private const val CARD_LOGO_TYPE = "card" private const val GOOGLE_PAY_LOGO_TYPE = PaymentMethodTypes.GOOGLE_PAY } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentMethodFragment.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentMethodFragment.kt index 1bcc7bbf13..ee7cb32148 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentMethodFragment.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentMethodFragment.kt @@ -19,10 +19,10 @@ import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.util.DateUtils +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.dropin.R import com.adyen.checkout.dropin.databinding.FragmentStoredPaymentMethodBinding import com.adyen.checkout.dropin.internal.provider.getComponentFor @@ -43,8 +43,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF private val storedPaymentViewModel: PreselectedStoredPaymentViewModel by viewModelsFactory { PreselectedStoredPaymentViewModel( storedPaymentMethod, - dropInViewModel.amount, - dropInViewModel.dropInConfiguration, + dropInViewModel.dropInParams, ) } @@ -64,7 +63,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Logger.d(TAG, "onViewCreated") + adyenLog(AdyenLogLevel.DEBUG) { "onViewCreated" } initView() observeState() @@ -77,10 +76,9 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF component = getComponentFor( fragment = this, storedPaymentMethod = storedPaymentMethod, - dropInConfiguration = dropInViewModel.dropInConfiguration, - amount = dropInViewModel.amount, + checkoutConfiguration = dropInViewModel.checkoutConfiguration, + dropInOverrideParams = dropInViewModel.getDropInOverrideParams(), componentCallback = storedPaymentViewModel, - sessionDetails = dropInViewModel.sessionDetails, analyticsRepository = dropInViewModel.analyticsRepository, onRedirect = protocol::onRedirect, ) @@ -93,7 +91,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF binding.paymentMethodsListHeader.paymentMethodHeaderTitle.setText(R.string.store_payment_methods_header) val isRemovingStoredPaymentMethodsEnabled = - dropInViewModel.dropInConfiguration.isRemovingStoredPaymentMethodsEnabled + dropInViewModel.dropInParams.isRemovingStoredPaymentMethodsEnabled binding.storedPaymentMethodItem.swipeToRevealLayout.setDragLocked(!isRemovingStoredPaymentMethodsEnabled) if (isRemovingStoredPaymentMethodsEnabled) { binding.storedPaymentMethodItem.paymentMethodItemUnderlayButton.setOnClickListener { @@ -112,7 +110,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF private fun observeState() { storedPaymentViewModel.uiStateFlow.onEach { state -> - Logger.v(TAG, "state: $state") + adyenLog(AdyenLogLevel.VERBOSE) { "state: $state" } updateStoredPaymentMethodItem(state.storedPaymentMethodModel) updateButtonState(state.buttonState) }.launchIn(viewLifecycleOwner.lifecycleScope) @@ -124,7 +122,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF binding.storedPaymentMethodItem.textViewTitle.text = requireActivity().getString(R.string.last_four_digits_format, storedPaymentMethodModel.lastFour) binding.storedPaymentMethodItem.imageViewLogo.loadLogo( - environment = dropInViewModel.dropInConfiguration.environment, + environment = dropInViewModel.dropInParams.environment, txVariant = storedPaymentMethodModel.imageId, ) binding.storedPaymentMethodItem.textViewDetail.text = @@ -136,10 +134,10 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF binding.storedPaymentMethodItem.textViewTitle.text = requireActivity().getString( R.string.last_four_digits_format, - storedPaymentMethodModel.lastFour + storedPaymentMethodModel.lastFour, ) binding.storedPaymentMethodItem.imageViewLogo.loadLogo( - environment = dropInViewModel.dropInConfiguration.environment, + environment = dropInViewModel.dropInParams.environment, txVariant = storedPaymentMethodModel.imageId, ) binding.storedPaymentMethodItem.textViewDetail.isVisible = false @@ -151,7 +149,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF !storedPaymentMethodModel.description.isNullOrEmpty() binding.storedPaymentMethodItem.textViewDetail.text = storedPaymentMethodModel.description binding.storedPaymentMethodItem.imageViewLogo.loadLogo( - environment = dropInViewModel.dropInConfiguration.environment, + environment = dropInViewModel.dropInParams.environment, txVariant = storedPaymentMethodModel.imageId, ) } @@ -203,7 +201,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF } private fun handleError(componentError: ComponentError) { - Logger.e(TAG, componentError.errorMessage) + adyenLog(AdyenLogLevel.ERROR) { componentError.errorMessage } protocol.showError(null, getString(R.string.component_error), componentError.errorMessage, true) } @@ -213,7 +211,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF .setMessage(R.string.checkout_remove_stored_payment_method_body) .setPositiveButton(R.string.checkout_giftcard_remove_gift_cards_positive_button) { dialog, _ -> val storedPaymentMethod = StoredPaymentMethod( - id = storedPaymentMethod.id + id = storedPaymentMethod.id, ) protocol.removeStoredPaymentMethod(storedPaymentMethod) dialog.dismiss() @@ -231,7 +229,6 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF } companion object { - private val TAG = LogUtil.getTag() private const val STORED_PAYMENT_KEY = "STORED_PAYMENT" @JvmStatic diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModel.kt index 16ae431946..efb2df1741 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModel.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModel.kt @@ -17,8 +17,8 @@ import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.dropin.DropInConfiguration import com.adyen.checkout.dropin.R +import com.adyen.checkout.dropin.internal.ui.model.DropInParams import com.adyen.checkout.dropin.internal.ui.model.StoredPaymentMethodModel import com.adyen.checkout.dropin.internal.util.mapStoredModel import kotlinx.coroutines.channels.Channel @@ -29,8 +29,7 @@ import java.util.Locale internal class PreselectedStoredPaymentViewModel( private val storedPaymentMethod: StoredPaymentMethod, - private val amount: Amount?, - private val dropInConfiguration: DropInConfiguration, + private val dropInParams: DropInParams, ) : ViewModel(), ComponentCallback> { private val _uiStateFlow = MutableStateFlow(getInitialUIState()) @@ -43,12 +42,12 @@ internal class PreselectedStoredPaymentViewModel( private fun getInitialUIState(): PreselectedStoredState { val storedPaymentMethodModel = storedPaymentMethod.mapStoredModel( - dropInConfiguration.isRemovingStoredPaymentMethodsEnabled, - dropInConfiguration.environment, + dropInParams.isRemovingStoredPaymentMethodsEnabled, + dropInParams.environment, ) return PreselectedStoredState( storedPaymentMethodModel = storedPaymentMethodModel, - buttonState = ButtonState.ContinueButton() + buttonState = ButtonState.ContinueButton(), ) } @@ -77,7 +76,7 @@ internal class PreselectedStoredPaymentViewModel( ButtonState.ContinueButton() } else { // component does not require user input -> treat it as a normal Pay button - ButtonState.PayButton(amount, dropInConfiguration.shopperLocale) + ButtonState.PayButton(dropInParams.amount, dropInParams.shopperLocale) } } diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInComponentParamsMapper.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInComponentParamsMapper.kt deleted file mode 100644 index 802955ab71..0000000000 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInComponentParamsMapper.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by josephj on 30/11/2022. - */ - -package com.adyen.checkout.dropin.internal.ui.model - -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.dropin.DropInConfiguration - -internal class DropInComponentParamsMapper { - - fun mapToParams( - dropInConfiguration: DropInConfiguration, - overrideAmount: Amount?, - ): DropInComponentParams { - with(dropInConfiguration) { - return DropInComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = true, - amount = overrideAmount, - showPreselectedStoredPaymentMethod = showPreselectedStoredPaymentMethod, - skipListWhenSinglePaymentMethod = skipListWhenSinglePaymentMethod, - isRemovingStoredPaymentMethodsEnabled = isRemovingStoredPaymentMethodsEnabled, - additionalDataForDropInService = additionalDataForDropInService, - ) - } - } -} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInOverrideParamsFactory.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInOverrideParamsFactory.kt new file mode 100644 index 0000000000..98505337a3 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInOverrideParamsFactory.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 8/2/2024. + */ + +package com.adyen.checkout.dropin.internal.ui.model + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails +import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory + +internal object DropInOverrideParamsFactory { + fun create(amount: Amount?, sessionDetails: SessionDetails?): DropInOverrideParams { + return DropInOverrideParams( + amount = amount, + sessionParams = sessionDetails?.let { SessionParamsFactory.create(sessionDetails) }, + ) + } +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInComponentParams.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInParams.kt similarity index 63% rename from drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInComponentParams.kt rename to drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInParams.kt index 59a24cff04..60efcf0aa9 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInComponentParams.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInParams.kt @@ -11,19 +11,18 @@ package com.adyen.checkout.dropin.internal.ui.model import android.os.Bundle import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.core.Environment import java.util.Locale -internal data class DropInComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, +internal data class DropInParams( + val shopperLocale: Locale, + val environment: Environment, + val clientKey: String, + val analyticsParams: AnalyticsParams, + val amount: Amount?, val showPreselectedStoredPaymentMethod: Boolean, val skipListWhenSinglePaymentMethod: Boolean, val isRemovingStoredPaymentMethodsEnabled: Boolean, val additionalDataForDropInService: Bundle?, -) : ComponentParams + val overriddenPaymentMethodInformation: Map, +) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInParamsMapper.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInParamsMapper.kt new file mode 100644 index 0000000000..1d74b86cd2 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInParamsMapper.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 30/11/2022. + */ + +package com.adyen.checkout.dropin.internal.ui.model + +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.dropin.getDropInConfiguration +import java.util.Locale + +internal class DropInParamsMapper { + + fun mapToParams( + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + sessionParams: SessionParams?, + ): DropInParams { + val dropInConfiguration = checkoutConfiguration.getDropInConfiguration() + return DropInParams( + shopperLocale = getShopperLocale(checkoutConfiguration, sessionParams) ?: deviceLocale, + environment = sessionParams?.environment ?: checkoutConfiguration.environment, + clientKey = sessionParams?.clientKey ?: checkoutConfiguration.clientKey, + analyticsParams = AnalyticsParams(checkoutConfiguration.analyticsConfiguration), + amount = sessionParams?.amount ?: checkoutConfiguration.amount, + showPreselectedStoredPaymentMethod = dropInConfiguration?.showPreselectedStoredPaymentMethod ?: true, + skipListWhenSinglePaymentMethod = dropInConfiguration?.skipListWhenSinglePaymentMethod ?: false, + isRemovingStoredPaymentMethodsEnabled = getIsRemovingStoredPaymentMethodsEnabled( + dropInConfiguration, + sessionParams, + ) ?: false, + additionalDataForDropInService = dropInConfiguration?.additionalDataForDropInService, + overriddenPaymentMethodInformation = dropInConfiguration?.overriddenPaymentMethodInformation.orEmpty(), + ) + } + + fun getShopperLocale(checkoutConfiguration: CheckoutConfiguration, sessionParams: SessionParams?): Locale? { + return checkoutConfiguration.shopperLocale ?: sessionParams?.shopperLocale + } + + private fun getIsRemovingStoredPaymentMethodsEnabled( + dropInConfiguration: DropInConfiguration?, + sessionParams: SessionParams? + ) = sessionParams?.showRemovePaymentMethodButton ?: dropInConfiguration?.isRemovingStoredPaymentMethodsEnabled +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInResultContractParams.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInResultContractParams.kt index 791469e7f0..4ea9d9a388 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInResultContractParams.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/DropInResultContractParams.kt @@ -8,14 +8,12 @@ package com.adyen.checkout.dropin.internal.ui.model -import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodsApiResponse -import com.adyen.checkout.dropin.DropInConfiguration import com.adyen.checkout.dropin.DropInService -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -data class DropInResultContractParams( - val dropInConfiguration: DropInConfiguration, +data class DropInResultContractParams internal constructor( + val checkoutConfiguration: CheckoutConfiguration, val paymentMethodsApiResponse: PaymentMethodsApiResponse, val serviceClass: Class, ) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/SessionDropInResultContractParams.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/SessionDropInResultContractParams.kt index 711a924895..d2f44c14d8 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/SessionDropInResultContractParams.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/SessionDropInResultContractParams.kt @@ -8,14 +8,12 @@ package com.adyen.checkout.dropin.internal.ui.model -import androidx.annotation.RestrictTo -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.dropin.SessionDropInService import com.adyen.checkout.sessions.core.CheckoutSession -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -data class SessionDropInResultContractParams( - val dropInConfiguration: DropInConfiguration, +data class SessionDropInResultContractParams internal constructor( + val checkoutConfiguration: CheckoutConfiguration, val checkoutSession: CheckoutSession, val serviceClass: Class, ) diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/CheckCompileOnly.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/CheckCompileOnly.kt new file mode 100644 index 0000000000..3998769aa0 --- /dev/null +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/CheckCompileOnly.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 8/12/2023. + */ + +package com.adyen.checkout.dropin.internal.util + +import com.adyen.checkout.core.internal.util.runCompileOnly + +internal inline fun checkCompileOnly(block: () -> Boolean): Boolean { + return runCompileOnly(block) ?: false +} diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/DropInPrefs.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/DropInPrefs.kt index 2385dceae1..ecad79cbc5 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/DropInPrefs.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/DropInPrefs.kt @@ -9,21 +9,20 @@ package com.adyen.checkout.dropin.internal.util import android.content.Context +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.internal.util.LocaleUtil -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import java.util.Locale internal object DropInPrefs { - private val TAG = LogUtil.getTag() private const val DROP_IN_PREFS = "drop-in-shared-prefs" private const val LOCALE_PREF = "drop-in-locale" - fun setShopperLocale(context: Context, shopperLocale: Locale) { - Logger.v(TAG, "setShopperLocale: $shopperLocale") - val localeTag = LocaleUtil.toLanguageTag(shopperLocale) - Logger.d(TAG, "Storing shopper locale tag: $localeTag") + fun setShopperLocale(context: Context, shopperLocale: Locale?) { + adyenLog(AdyenLogLevel.VERBOSE) { "setShopperLocale: $shopperLocale" } + val localeTag = shopperLocale?.let { LocaleUtil.toLanguageTag(shopperLocale) } + adyenLog(AdyenLogLevel.DEBUG) { "Storing shopper locale tag: $localeTag" } return context .getSharedPreferences(DROP_IN_PREFS, Context.MODE_PRIVATE) .edit() @@ -31,15 +30,15 @@ internal object DropInPrefs { .apply() } - fun getShopperLocale(context: Context): Locale { - Logger.v(TAG, "getShopperLocale") + fun getShopperLocale(context: Context): Locale? { + adyenLog(AdyenLogLevel.VERBOSE) { "getShopperLocale" } val localeTag = context .getSharedPreferences(DROP_IN_PREFS, Context.MODE_PRIVATE) .getString(LOCALE_PREF, null) - .orEmpty() - Logger.d(TAG, "Fetching shopper locale tag: $localeTag") + adyenLog(AdyenLogLevel.DEBUG) { "Fetched shopper locale tag: $localeTag" } + if (localeTag == null) return null val locale = LocaleUtil.fromLanguageTag(localeTag) - Logger.d(TAG, "Parsed locale: $locale") + adyenLog(AdyenLogLevel.DEBUG) { "Parsed locale: $locale" } return locale } } diff --git a/drop-in/src/main/res/template/values/strings.xml.tt b/drop-in/src/main/res/template/values/strings.xml.tt index 4580769cbb..535f554cda 100644 --- a/drop-in/src/main/res/template/values/strings.xml.tt +++ b/drop-in/src/main/res/template/values/strings.xml.tt @@ -8,12 +8,10 @@ %%paymentMethods.title%% - %%error.title%% - %%dismissButton%% %%resultMessages.failed%% %%resultMessages.Error%% - %%resultMessages.unknown%% %%resultMessages.timeout%% + %%error.message.unknown%% %%paymentMethods.storedMethods%% %%paymentMethods.otherMethods%% %%continue%% diff --git a/drop-in/src/main/res/values-ar/strings.xml b/drop-in/src/main/res/values-ar/strings.xml index b77e9fb9cf..13a406a101 100644 --- a/drop-in/src/main/res/values-ar/strings.xml +++ b/drop-in/src/main/res/values-ar/strings.xml @@ -8,12 +8,10 @@ طرق الدفع - خطأ - موافق خطأ في إرسال المدفوعات. يُرجى المحاولة مرة أخرى. حدث خطأ أثناء معالجة مدفوعاتك. يُرجى المحاولة مرة أخرى لاحقًا. - حدث خطأ ما. انتهت مهلة الطلب. يُرجى المحاولة مرة أخرى. + حدث خطأ غير معروف طرق الدفع الخاصة بك تحديد طريقة أخرى متابعة diff --git a/drop-in/src/main/res/values-cs-rCZ/strings.xml b/drop-in/src/main/res/values-cs-rCZ/strings.xml index 32cd19e71a..5bb2c702b3 100644 --- a/drop-in/src/main/res/values-cs-rCZ/strings.xml +++ b/drop-in/src/main/res/values-cs-rCZ/strings.xml @@ -8,12 +8,10 @@ Způsoby platby - Chyba - OK Chyba při odesílání platby. Zkuste to znovu. Při zpracování platby došlo k chybě. Zkuste to znovu později. - Něco se pokazilo. Vypršení doby požadavku. Zkuste to znovu. + Došlo k neznámé chybě Vaše způsoby platby Vyberte jiný způsob platby Pokračovat diff --git a/drop-in/src/main/res/values-da-rDK/strings.xml b/drop-in/src/main/res/values-da-rDK/strings.xml index 673daba97f..d8dc499f76 100644 --- a/drop-in/src/main/res/values-da-rDK/strings.xml +++ b/drop-in/src/main/res/values-da-rDK/strings.xml @@ -8,12 +8,10 @@ Betalingsmåder - Fejl - OK Fejl ved gennemførelse af betaling. Prøv igen. Der opstod en fejl ved behandlingen af betalingen. Prøv igen senere. - Noget gik galt. Anmodning udløbet. Prøv igen. + Der opstod en ukendt fejl Dine betalingsmåder Vælg anden måde Fortsæt diff --git a/drop-in/src/main/res/values-de-rDE/strings.xml b/drop-in/src/main/res/values-de-rDE/strings.xml index 0ea98931c7..b9924e8605 100644 --- a/drop-in/src/main/res/values-de-rDE/strings.xml +++ b/drop-in/src/main/res/values-de-rDE/strings.xml @@ -8,12 +8,10 @@ Zahlungsmethoden - Fehler - OK Fehler beim Senden der Zahlung. Bitte versuchen Sie es erneut. Bei der Verarbeitung Ihrer Zahlung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut. - Es ist etwas schief gegangen. Zeitüberschreitung bei Anfrage. Bitte versuchen Sie es erneut. + Es ist ein unbekannter Fehler aufgetreten. Ihre Zahlungsmethoden Andere Zahlungsmethoden Weiter diff --git a/drop-in/src/main/res/values-el-rGR/strings.xml b/drop-in/src/main/res/values-el-rGR/strings.xml index c62fd82e3a..3cd130f2d8 100644 --- a/drop-in/src/main/res/values-el-rGR/strings.xml +++ b/drop-in/src/main/res/values-el-rGR/strings.xml @@ -8,12 +8,10 @@ Τρόποι πληρωμής - Σφάλμα - ΟΚ Σφάλμα κατά την αποστολή πληρωμής. Προσπαθήστε ξανά. Προέκυψε σφάλμα κατά την επεξεργασία της πληρωμής σας. Προσπαθήστε ξανά αργότερα. - Κάτι πήγε στραβά. Έληξε το χρονικό όριο του αιτήματος. Προσπαθήστε ξανά. + Προέκυψε άγνωστο σφάλμα Οι τρόποι πληρωμής σας Επιλέξτε άλλον τρόπο Συνέχεια diff --git a/drop-in/src/main/res/values-es-rES/strings.xml b/drop-in/src/main/res/values-es-rES/strings.xml index 22ca001701..274cc9cc36 100644 --- a/drop-in/src/main/res/values-es-rES/strings.xml +++ b/drop-in/src/main/res/values-es-rES/strings.xml @@ -8,12 +8,10 @@ Métodos de pago - Error - Okay Error al enviar el pago. Por favor, vuelva a intentarlo. Se produjo un error al procesar su pago. Inténtelo de nuevo más tarde. - Algo ha salido mal. Se ha superado el tiempo de espera. Por favor, vuelva a intentarlo. + Se ha producido un error desconocido Tus metodos de pago Otros métodos de pago Continuar diff --git a/drop-in/src/main/res/values-fi-rFI/strings.xml b/drop-in/src/main/res/values-fi-rFI/strings.xml index d626771d73..1c5cf4c9b9 100644 --- a/drop-in/src/main/res/values-fi-rFI/strings.xml +++ b/drop-in/src/main/res/values-fi-rFI/strings.xml @@ -8,12 +8,10 @@ Maksutavat - Virhe - Ok Virhe maksun lähettämisessä. Yritä uudelleen. Maksua käsitellessä tapahtui virhe. Yritä myöhemmin uudelleen. - Jotain meni pieleen. Pyydä aikakatkaisu. Yritä uudelleen. + Tapahtui tuntematon virhe Maksutapasi Valitse muu maksutapa Jatka diff --git a/drop-in/src/main/res/values-fr-rFR/strings.xml b/drop-in/src/main/res/values-fr-rFR/strings.xml index 12ea1df208..d9724e92cd 100644 --- a/drop-in/src/main/res/values-fr-rFR/strings.xml +++ b/drop-in/src/main/res/values-fr-rFR/strings.xml @@ -8,12 +8,10 @@ Méthodes de paiement - Erreur - Ok Erreur lors de l\'envoi du paiement. Veuillez réessayer. Une erreur s\'est produite lors du traitement de votre paiement. Veuillez réessayer plus tard. - Un problème s\'est produit. La requête a expiré. Veuillez réessayer. + Une erreur inconnue s\'est produite Vos modes de paiement Autre méthode de paiement Continuer diff --git a/drop-in/src/main/res/values-hr-rHR/strings.xml b/drop-in/src/main/res/values-hr-rHR/strings.xml index 482f3b2b53..fe021bb240 100644 --- a/drop-in/src/main/res/values-hr-rHR/strings.xml +++ b/drop-in/src/main/res/values-hr-rHR/strings.xml @@ -8,12 +8,10 @@ Način plaćanja - Greška - U redu Greška prilikom slanja plaćanja. Pokušajte ponovno. Došlo je do pogreške prilikom obrade plaćanja. Pokušajte ponovno kasnije. - Pojavio se problem. Isteklo je vrijeme zahtjeva. Pokušajte ponovno. + Dogodila se nepoznata greška Vaš način plaćanja Odaberite drugi način Nastavi diff --git a/drop-in/src/main/res/values-hu-rHU/strings.xml b/drop-in/src/main/res/values-hu-rHU/strings.xml index d302213593..70cbc5b161 100644 --- a/drop-in/src/main/res/values-hu-rHU/strings.xml +++ b/drop-in/src/main/res/values-hu-rHU/strings.xml @@ -8,12 +8,10 @@ Fizetési módok - Hiba - OK Hiba történt a fizetés elküldése során. Próbálkozzon újra. Fizetése feldolgozása során hiba történt. Próbálkozzon újra később. - Hiba történt. A kérés időkorlátja lejárt. Próbálkozzon újra. + Ismeretlen hiba történt Fizetési módjai Másik mód választása Folytatás diff --git a/drop-in/src/main/res/values-it-rIT/strings.xml b/drop-in/src/main/res/values-it-rIT/strings.xml index 985d7ec7ac..902739f207 100644 --- a/drop-in/src/main/res/values-it-rIT/strings.xml +++ b/drop-in/src/main/res/values-it-rIT/strings.xml @@ -8,12 +8,10 @@ Metodo di Pagamento - Errore - OK Errore durante l\'invio del pagamento. Riprova. Si è verificato un errore durante l\'elaborazione del pagamento. Riprova più tardi. - Si è verificato un errore. Il tempo disponibile per la richiesta è scaduto. Riprova. + Si è verificato un errore sconosciuto Metodi di pagamento Scegli altro metodo di pagamento Continua diff --git a/drop-in/src/main/res/values-ja-rJP/strings.xml b/drop-in/src/main/res/values-ja-rJP/strings.xml index 86116813ca..b09a1a00cb 100644 --- a/drop-in/src/main/res/values-ja-rJP/strings.xml +++ b/drop-in/src/main/res/values-ja-rJP/strings.xml @@ -8,12 +8,10 @@ お支払い方法 - エラー - OK 支払いの送信中にエラーが発生しました。再度お試し下さい。 支払の処理中にエラーが発生しました。後でもう一度やり直してください。 - 問題が発生しました。 リクエストがタイムアウトになりました。再度お試し下さい。 + 不明なエラーが発生しました 選択済みのお支払い方法 その他のお支払いオプション 続ける diff --git a/drop-in/src/main/res/values-ko-rKR/strings.xml b/drop-in/src/main/res/values-ko-rKR/strings.xml index 7493dfd29e..9838f05771 100644 --- a/drop-in/src/main/res/values-ko-rKR/strings.xml +++ b/drop-in/src/main/res/values-ko-rKR/strings.xml @@ -8,12 +8,10 @@ 결제 수단 - 오류 - 확인 결제 전송 중 오류 발생. 다시 시도해 주세요. 결제 처리 중 오류가 발생했습니다. 나중에 다시 시도해 주세요. - 오류가 발생했습니다. 요청 시간 초과. 다시 시도해 주세요. + 알 수 없는 오류 발생 귀하의 결제 수단 다른 수단 선택 계속 diff --git a/drop-in/src/main/res/values-nb-rNO/strings.xml b/drop-in/src/main/res/values-nb-rNO/strings.xml index 8b678fbc28..170f31d8b7 100644 --- a/drop-in/src/main/res/values-nb-rNO/strings.xml +++ b/drop-in/src/main/res/values-nb-rNO/strings.xml @@ -8,12 +8,10 @@ Betalingsmetoder - Feil - Ok Feil ved sending av betaling. Vennligst prøv igjen. Det oppstod en feil under behandlingen av betalingen din. Vennligst prøv igjen senere. - Noe gikk galt. Forespørselen fikk tidsavbrudd. Vennligst prøv igjen. + En ukjent feil oppstod Dine betalingsmetoder Velg annen metode Fortsett diff --git a/drop-in/src/main/res/values-nl-rNL/strings.xml b/drop-in/src/main/res/values-nl-rNL/strings.xml index 8bc6e553b5..a1fe68ced9 100644 --- a/drop-in/src/main/res/values-nl-rNL/strings.xml +++ b/drop-in/src/main/res/values-nl-rNL/strings.xml @@ -8,12 +8,10 @@ Betaalmethodes - Fout - Oké Fout bij het verzenden van betaling. Probeer het opnieuw. Er is een fout opgetreden bij het verwerken van uw betaling. Probeer het later opnieuw. - Er is iets misgegaan. Verzoek is verlopen. Probeer het opnieuw. + Er is een onbekende fout opgetreden Opgeslagen betaalmethodes Alle betaalmethodes Doorgaan diff --git a/drop-in/src/main/res/values-pl-rPL/strings.xml b/drop-in/src/main/res/values-pl-rPL/strings.xml index 47e882d45c..437d6d8798 100644 --- a/drop-in/src/main/res/values-pl-rPL/strings.xml +++ b/drop-in/src/main/res/values-pl-rPL/strings.xml @@ -8,12 +8,10 @@ Metody płatności - Błąd - Ok Wystąpił błąd podczas wysyłania płatności. Spróbuj ponownie. Podczas przetwarzania płatności wystąpił błąd. Prosimy spróbować ponownie. - Coś poszło nie tak. Upłynął limit czasu żądania. Spróbuj ponownie. + Wystąpił nieoczekiwany błąd Twoje metody płatności Wybierz inną metodę Kontynuuj diff --git a/drop-in/src/main/res/values-pt-rBR/strings.xml b/drop-in/src/main/res/values-pt-rBR/strings.xml index 9d4d1543d8..8fbef55e62 100644 --- a/drop-in/src/main/res/values-pt-rBR/strings.xml +++ b/drop-in/src/main/res/values-pt-rBR/strings.xml @@ -8,12 +8,10 @@ Meios de pagamento - Erro - OK Erro ao enviar o pagamento. Tente novamente. Ocorreu um erro ao processar o seu pagamento. Tente novamente mais tarde. - Algo deu errado. O pedido expirou. Tente novamente. + Ocorreu um erro desconhecido Meus métodos de pagamento Todos os métodos de pagamento Continuar diff --git a/drop-in/src/main/res/values-pt-rPT/strings.xml b/drop-in/src/main/res/values-pt-rPT/strings.xml index 341287fd3f..63d919b12a 100644 --- a/drop-in/src/main/res/values-pt-rPT/strings.xml +++ b/drop-in/src/main/res/values-pt-rPT/strings.xml @@ -8,12 +8,10 @@ forma de pagamento - Erro - OK Erro ao enviar pagamento. Tente novamente Ocorreu um erro ao processar o seu pagamento. Tente novamente mais tarde. - Algo correu mal. Pedido com tempo limite. Tente novamente + Ocorreu um erro desconhecido Os seus métodos de pagamento Selecione outro método Continuar diff --git a/drop-in/src/main/res/values-ro-rRO/strings.xml b/drop-in/src/main/res/values-ro-rRO/strings.xml index 77e0602cbc..36096cee9c 100644 --- a/drop-in/src/main/res/values-ro-rRO/strings.xml +++ b/drop-in/src/main/res/values-ro-rRO/strings.xml @@ -8,12 +8,10 @@ Metode de plată - Eroare - Ok Eroare la trimiterea plății. Vă rugăm să încercați din nou. S-a produs o eroare la prelucrarea plății. Vă rugăm să încercați din nou mai târziu. - A apărut o problemă. Solicitarea a expirat. Vă rugăm să încercați din nou. + S-a produs o eroare necunoscută Metodele dvs. de plată Selectați o altă metodă Continuare diff --git a/drop-in/src/main/res/values-ru-rRU/strings.xml b/drop-in/src/main/res/values-ru-rRU/strings.xml index 6642a8be93..fcca8ba4b3 100644 --- a/drop-in/src/main/res/values-ru-rRU/strings.xml +++ b/drop-in/src/main/res/values-ru-rRU/strings.xml @@ -8,12 +8,10 @@ Способы оплаты - Ошибка - ОК Ошибка при отправке платежа. Повторите попытку. При обработке платежа возникла ошибка. Повторите попытку позже. - Произошла ошибка. Время ожидания запроса истекло. Повторите попытку. + Возникла неизвестная ошибка Ваши способы оплаты Выбрать другой способ Продолжить diff --git a/drop-in/src/main/res/values-sk-rSK/strings.xml b/drop-in/src/main/res/values-sk-rSK/strings.xml index 5477f4e7bc..17c07634d7 100644 --- a/drop-in/src/main/res/values-sk-rSK/strings.xml +++ b/drop-in/src/main/res/values-sk-rSK/strings.xml @@ -8,12 +8,10 @@ Spôsob platby - Chyba - OK Pri odosielaní platby došlo k chybe. Skúste to znova. Pri spracúvaní vašej platby sa vyskytla chyba. Skúste to neskôr znova. - Niečo sa pokazilo. Časový limit pre požiadavku uplynul. Skúste to znova. + Vyskytla sa neznáma chyba Vaše spôsoby platby Vyberte iný spôsob Pokračovať diff --git a/drop-in/src/main/res/values-sl-rSI/strings.xml b/drop-in/src/main/res/values-sl-rSI/strings.xml index b66f88f295..647aff2481 100644 --- a/drop-in/src/main/res/values-sl-rSI/strings.xml +++ b/drop-in/src/main/res/values-sl-rSI/strings.xml @@ -8,12 +8,10 @@ Načini plačila - Napaka - OK Napaka pri pošiljanju plačila. Poskusite znova. Pri obdelavi vašega plačila je prišlo do napake. Poskusite znova pozneje. - Nekaj je šlo narobe. Zahteva je potekla. Poskusite znova. + Prišlo je do neznane napake Vaši načini plačila Izberite drug način Nadaljuj diff --git a/drop-in/src/main/res/values-sv-rSE/strings.xml b/drop-in/src/main/res/values-sv-rSE/strings.xml index abddf49a17..4226431eeb 100644 --- a/drop-in/src/main/res/values-sv-rSE/strings.xml +++ b/drop-in/src/main/res/values-sv-rSE/strings.xml @@ -8,12 +8,10 @@ Betalningsmetoder - Fel - OK Fel när betalningen skickades. Försök igen. Det uppstod ett fel vid behandlingen av din betalning. Försök igen senare. - Någonting gick fel. Förfrågan tog för lång tid. Försök igen. + Ett okänt fel uppstod Dina sparade betalningsmetoder Välj annan betalningsmetod Fortsätt diff --git a/drop-in/src/main/res/values-zh-rCN/strings.xml b/drop-in/src/main/res/values-zh-rCN/strings.xml index f3990424a8..46ad47e1a1 100644 --- a/drop-in/src/main/res/values-zh-rCN/strings.xml +++ b/drop-in/src/main/res/values-zh-rCN/strings.xml @@ -8,12 +8,10 @@ 支付方式 - 错误 - 确定 付款时出错。请重试。 处理您的付款时发生错误。请稍候重试。 - 发生错误。 请求超时。请重试。 + 发生未知错误 您的支付方式 选择其他方式 继续 diff --git a/drop-in/src/main/res/values-zh-rTW/strings.xml b/drop-in/src/main/res/values-zh-rTW/strings.xml index 12698ba16a..6f2e08181e 100644 --- a/drop-in/src/main/res/values-zh-rTW/strings.xml +++ b/drop-in/src/main/res/values-zh-rTW/strings.xml @@ -8,12 +8,10 @@ 付款方式 - 錯誤 - 確定 傳送付款時出錯。請再試一次。 處理您的付款時發生錯誤。請稍後再試。 - 出了些問題。 請求已逾時。請再試一次。 + 發生未知錯誤 您的付款方式 選取其他方式 繼續 diff --git a/drop-in/src/main/res/values/strings.xml b/drop-in/src/main/res/values/strings.xml index 41c42e7c67..c3305a8f66 100644 --- a/drop-in/src/main/res/values/strings.xml +++ b/drop-in/src/main/res/values/strings.xml @@ -8,12 +8,10 @@ Payment Methods - Error - OK Error sending payment. Please try again. There was an error while processing your payment. Please try again later. - Something went wrong. Request timed out. Please try again. + An unknown error occurred Your payment methods Select other method Continue @@ -38,4 +36,4 @@ - %s •••• %s - + \ No newline at end of file diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/DropInConfigurationTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/DropInConfigurationTest.kt new file mode 100644 index 0000000000..3bcb49bc20 --- /dev/null +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/DropInConfigurationTest.kt @@ -0,0 +1,143 @@ +package com.adyen.checkout.dropin + +import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration +import com.adyen.checkout.adyen3ds2.getAdyen3DS2Configuration +import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.card.getCardConfiguration +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.googlepay.getGooglePayConfiguration +import com.adyen.checkout.ideal.IdealConfiguration +import com.adyen.checkout.ideal.getIdealConfiguration +import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.redirect.getRedirectConfiguration +import com.adyen.checkout.wechatpay.WeChatPayActionConfiguration +import com.adyen.checkout.wechatpay.getWeChatPayActionConfiguration +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class DropInConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + dropIn { + setShowPreselectedStoredPaymentMethod(false) + setSkipListWhenSinglePaymentMethod(true) + setEnableRemovingStoredPaymentMethods(true) + overridePaymentMethodName("mc", "MC") + } + } + + val actual = checkoutConfiguration.getDropInConfiguration() + + val expected = DropInConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setShowPreselectedStoredPaymentMethod(false) + .setSkipListWhenSinglePaymentMethod(true) + .setEnableRemovingStoredPaymentMethods(true) + .overridePaymentMethodName("mc", "MC") + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.showPreselectedStoredPaymentMethod, actual?.showPreselectedStoredPaymentMethod) + assertEquals(expected.skipListWhenSinglePaymentMethod, actual?.skipListWhenSinglePaymentMethod) + assertEquals(expected.isRemovingStoredPaymentMethodsEnabled, actual?.isRemovingStoredPaymentMethodsEnabled) + assertEquals(expected.overriddenPaymentMethodInformation, actual?.overriddenPaymentMethodInformation) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = DropInConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setShowPreselectedStoredPaymentMethod(false) + .setSkipListWhenSinglePaymentMethod(true) + .setEnableRemovingStoredPaymentMethods(true) + .overridePaymentMethodName("mc", "MC") + .addCardConfiguration(CardConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build()) + .addGooglePayConfiguration( + GooglePayConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addIdealConfiguration(IdealConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build()) + .add3ds2ActionConfiguration( + Adyen3DS2Configuration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addRedirectActionConfiguration( + RedirectConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build(), + ) + .addWeChatPayActionConfiguration( + WeChatPayActionConfiguration.Builder( + Locale.US, + Environment.TEST, + TEST_CLIENT_KEY, + ).build(), + ) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualDropInConfig = actual.getDropInConfiguration() + assertEquals(config.shopperLocale, actualDropInConfig?.shopperLocale) + assertEquals(config.environment, actualDropInConfig?.environment) + assertEquals(config.clientKey, actualDropInConfig?.clientKey) + assertEquals(config.amount, actualDropInConfig?.amount) + assertEquals(config.analyticsConfiguration, actualDropInConfig?.analyticsConfiguration) + assertEquals(config.showPreselectedStoredPaymentMethod, actualDropInConfig?.showPreselectedStoredPaymentMethod) + assertEquals(config.skipListWhenSinglePaymentMethod, actualDropInConfig?.skipListWhenSinglePaymentMethod) + assertEquals( + config.isRemovingStoredPaymentMethodsEnabled, + actualDropInConfig?.isRemovingStoredPaymentMethodsEnabled, + ) + assertEquals(config.overriddenPaymentMethodInformation, actualDropInConfig?.overriddenPaymentMethodInformation) + assertNotNull(actual.getCardConfiguration()) + assertNotNull(actual.getGooglePayConfiguration()) + assertNotNull(actual.getIdealConfiguration()) + assertNotNull(actual.getAdyen3DS2Configuration()) + assertNotNull(actual.getRedirectConfiguration()) + assertNotNull(actual.getWeChatPayActionConfiguration()) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt index e15189725d..ea8582bf30 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ConfigurationProvider.kt @@ -8,53 +8,38 @@ package com.adyen.checkout.dropin.internal -import com.adyen.checkout.bcmc.BcmcConfiguration -import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.bcmc.bcmc +import com.adyen.checkout.card.card import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.dropin.DropInConfiguration -import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.dropin.dropIn +import com.adyen.checkout.googlepay.googlePay import java.util.Locale internal object ConfigurationProvider { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" - private val TAG = LogUtil.getTag() private val shopperLocale = Locale.US private val amount = Amount(currency = "EUR", value = 1337) private val environment = Environment.TEST - internal fun getDropInConfiguration(): DropInConfiguration { - val dropInConfigurationBuilder = DropInConfiguration.Builder( - shopperLocale, - environment, - TEST_CLIENT_KEY, - ) - .addCardConfiguration(getCardConfiguration()) - .addBcmcConfiguration(getBcmcConfiguration()) - .addGooglePayConfiguration(getGooglePayConfiguration()) - .setEnableRemovingStoredPaymentMethods(true) - - try { - dropInConfigurationBuilder.setAmount(amount) - } catch (e: CheckoutException) { - Logger.e(TAG, "Amount $amount not valid", e) + fun getCheckoutConfiguration() = CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + dropIn { + setEnableRemovingStoredPaymentMethods(true) } - return dropInConfigurationBuilder.build() - } - - private fun getCardConfiguration(): CardConfiguration = - CardConfiguration.Builder(shopperLocale, environment, TEST_CLIENT_KEY).build() + card() - private fun getBcmcConfiguration(): BcmcConfiguration = - BcmcConfiguration.Builder(shopperLocale, environment, TEST_CLIENT_KEY).build() + bcmc() - private fun getGooglePayConfiguration(): GooglePayConfiguration = - GooglePayConfiguration.Builder(shopperLocale, environment, TEST_CLIENT_KEY) - .setCountryCode("NL") - .setAmount(amount) - .build() + googlePay { + setCountryCode("NL") + } + } } diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt index 51a02eee66..61244b548d 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodsListViewModelTest.kt @@ -9,26 +9,25 @@ package com.adyen.checkout.dropin.internal.ui import android.app.Application -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.StoredPaymentMethod -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.dropin.internal.ConfigurationProvider import com.adyen.checkout.dropin.internal.DataProvider import com.adyen.checkout.dropin.internal.Helpers.mapToPaymentMethodModelList import com.adyen.checkout.dropin.internal.Helpers.mapToStoredPaymentMethodsModelList +import com.adyen.checkout.dropin.internal.ui.model.DropInParamsMapper import com.adyen.checkout.dropin.internal.ui.model.GiftCardPaymentMethodModel import com.adyen.checkout.dropin.internal.ui.model.OrderModel import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodHeader import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodModel import com.adyen.checkout.dropin.internal.ui.model.PaymentMethodNote import com.adyen.checkout.dropin.internal.ui.model.StoredPaymentMethodModel -import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails import com.adyen.checkout.test.TestDispatcherExtension import kotlinx.coroutines.test.runTest -import org.junit.Rule import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue @@ -42,47 +41,25 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.whenever +import java.util.Locale @ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) internal class PaymentMethodsListViewModelTest( @Mock private val application: Application ) { - @get:Rule - var instantTaskExecutorRule = InstantTaskExecutorRule() - - private lateinit var configuration: DropInConfiguration - private lateinit var amount: Amount - private lateinit var paymentMethods: List - private lateinit var storedPaymentMethods: MutableList private lateinit var viewModel: PaymentMethodsListViewModel - private var order: OrderModel? = null - private var sessionDetails: SessionDetails? = null @BeforeEach fun setup() { - configuration = ConfigurationProvider.getDropInConfiguration() - amount = Amount(currency = "EUR", value = 1234567) - paymentMethods = DataProvider.getPaymentMethodsList() - storedPaymentMethods = DataProvider.getStoredPaymentMethods().toMutableList() - order = DataProvider.getOrder() whenever(application.getString(any(), any())) doReturn "string" - viewModel = PaymentMethodsListViewModel( - application = application, - paymentMethods = paymentMethods, - storedPaymentMethods = storedPaymentMethods, - order = order, - dropInConfiguration = configuration, - amount = amount, - sessionDetails = sessionDetails, - ) + viewModel = createViewModel() } @Test fun `test remove stored payment method success`() = runTest { viewModel.paymentMethodsFlow.test { - val paymentMethods = storedPaymentMethods - .mapToStoredPaymentMethodsModelList(configuration.isRemovingStoredPaymentMethodsEnabled) + val paymentMethods = DataProvider.getStoredPaymentMethods().mapToStoredPaymentMethodsModelList(true) val storedPaymentMethod = paymentMethods[0] viewModel.removePaymentMethodWithId(storedPaymentMethod.id) @@ -95,11 +72,16 @@ internal class PaymentMethodsListViewModelTest( @Test fun `test get payment method from payment method model success`() { + val paymentMethod = PaymentMethod(type = "test", name = "Test pm") + val paymentMethods = listOf(paymentMethod) + viewModel = createViewModel( + paymentMethods = paymentMethods, + ) val paymentMethodModelList = paymentMethods.mapToPaymentMethodModelList() - val paymentMethod = viewModel.getPaymentMethod(paymentMethodModelList[0]) + val actual = viewModel.getPaymentMethod(paymentMethodModelList[0]) - assertEquals(paymentMethod, paymentMethods[0]) + assertEquals(paymentMethod, actual) } @Nested @@ -121,17 +103,7 @@ internal class PaymentMethodsListViewModelTest( @Test fun `payment methods list is empty, then payment method flow won't contain payment methods`() = runTest { - paymentMethods = emptyList() - - viewModel = PaymentMethodsListViewModel( - application = application, - paymentMethods = paymentMethods, - storedPaymentMethods = storedPaymentMethods, - order = order, - dropInConfiguration = configuration, - amount = amount, - sessionDetails = sessionDetails, - ) + viewModel = createViewModel(paymentMethods = emptyList()) viewModel.paymentMethodsFlow.test { with(expectMostRecentItem()) { @@ -147,17 +119,7 @@ internal class PaymentMethodsListViewModelTest( @Test fun `stored payment methods list is empty, then payment method flow won't contain stored payment methods`() = runTest { - storedPaymentMethods = mutableListOf() - - viewModel = PaymentMethodsListViewModel( - application = application, - paymentMethods = paymentMethods, - storedPaymentMethods = storedPaymentMethods, - order = order, - dropInConfiguration = configuration, - amount = amount, - sessionDetails = sessionDetails, - ) + viewModel = createViewModel(storedPaymentMethods = emptyList()) viewModel.paymentMethodsFlow.test { with(expectMostRecentItem()) { @@ -171,45 +133,26 @@ internal class PaymentMethodsListViewModelTest( } @Test - fun `order is null, then payment method flow won't contain gift cards or payment method note`() = - runTest { - order = null - - viewModel = PaymentMethodsListViewModel( - application = application, - paymentMethods = paymentMethods, - storedPaymentMethods = storedPaymentMethods, - order = order, - dropInConfiguration = configuration, - amount = amount, - sessionDetails = sessionDetails, - ) + fun `order is null, then payment method flow won't contain gift cards or payment method note`() = runTest { + viewModel = createViewModel(order = null) - viewModel.paymentMethodsFlow.test { - with(expectMostRecentItem()) { - assertTrue(filterIsInstance().isNotEmpty()) - assertTrue(filterIsInstance().isNotEmpty()) - assertTrue(filterIsInstance().isNotEmpty()) - assertTrue(filterIsInstance().isEmpty()) - assertTrue(filterIsInstance().isEmpty()) - } + viewModel.paymentMethodsFlow.test { + with(expectMostRecentItem()) { + assertTrue(filterIsInstance().isNotEmpty()) + assertTrue(filterIsInstance().isNotEmpty()) + assertTrue(filterIsInstance().isNotEmpty()) + assertTrue(filterIsInstance().isEmpty()) + assertTrue(filterIsInstance().isEmpty()) } } + } @Test fun `stored and normal payment methods lists are empty, then payment method flow won't contain stored or normal payment methods`() = runTest { - storedPaymentMethods = mutableListOf() - paymentMethods = emptyList() - - viewModel = PaymentMethodsListViewModel( - application = application, - paymentMethods = paymentMethods, - storedPaymentMethods = storedPaymentMethods, - order = order, - dropInConfiguration = configuration, - amount = amount, - sessionDetails = sessionDetails, + viewModel = createViewModel( + storedPaymentMethods = emptyList(), + paymentMethods = emptyList(), ) viewModel.paymentMethodsFlow.test { @@ -224,27 +167,34 @@ internal class PaymentMethodsListViewModelTest( } @Test - fun `all payment methods are empty, then payment method flow will be empty`() = - runTest { - storedPaymentMethods = mutableListOf() - paymentMethods = emptyList() - order = null - - viewModel = PaymentMethodsListViewModel( - application = application, - paymentMethods = paymentMethods, - storedPaymentMethods = storedPaymentMethods, - order = order, - dropInConfiguration = configuration, - amount = amount, - sessionDetails = sessionDetails, - ) + fun `all payment methods are empty, then payment method flow will be empty`() = runTest { + viewModel = createViewModel( + storedPaymentMethods = emptyList(), + paymentMethods = emptyList(), + order = null, + ) - viewModel.paymentMethodsFlow.test { - with(expectMostRecentItem()) { - assertTrue(isEmpty()) - } + viewModel.paymentMethodsFlow.test { + with(expectMostRecentItem()) { + assertTrue(isEmpty()) } } + } } + + private fun createViewModel( + checkoutConfiguration: CheckoutConfiguration = ConfigurationProvider.getCheckoutConfiguration(), + amount: Amount = Amount(currency = "EUR", value = 1234567), + paymentMethods: List = DataProvider.getPaymentMethodsList(), + storedPaymentMethods: List = DataProvider.getStoredPaymentMethods(), + order: OrderModel? = DataProvider.getOrder(), + ) = PaymentMethodsListViewModel( + application = application, + paymentMethods = paymentMethods, + storedPaymentMethods = storedPaymentMethods, + order = order, + checkoutConfiguration = checkoutConfiguration, + dropInParams = DropInParamsMapper().mapToParams(checkoutConfiguration, Locale.US, null), + dropInOverrideParams = DropInOverrideParams(amount, null), + ) } diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt index 3f6988e16d..6509294cb3 100644 --- a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/PreselectedStoredPaymentViewModelTest.kt @@ -11,15 +11,16 @@ package com.adyen.checkout.dropin.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.dropin.dropIn +import com.adyen.checkout.dropin.internal.ui.model.DropInParamsMapper import com.adyen.checkout.dropin.internal.ui.model.GenericStoredModel import com.adyen.checkout.test.TestDispatcherExtension -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -31,11 +32,14 @@ import org.junit.jupiter.api.fail import org.mockito.junit.jupiter.MockitoExtension import java.util.Locale -@OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) internal class PreselectedStoredPaymentViewModelTest { private val storedPaymentMethod = StoredPaymentMethod() - private val dropInConfiguration = DropInConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY).build() + private val dropInParams = DropInParamsMapper().mapToParams( + checkoutConfiguration = CheckoutConfiguration(Environment.TEST, TEST_CLIENT_KEY, amount = TEST_AMOUNT), + deviceLocale = Locale.US, + sessionParams = null, + ) private lateinit var viewModel: PreselectedStoredPaymentViewModel @@ -43,25 +47,30 @@ internal class PreselectedStoredPaymentViewModelTest { fun setup() { viewModel = PreselectedStoredPaymentViewModel( storedPaymentMethod, - TEST_AMOUNT, - dropInConfiguration, + dropInParams, ) } @Test fun `when view model is initialized then uiStateFlow has a matching initial value`() = runTest { - val dropInConfiguration = DropInConfiguration.Builder( - Locale.US, + val checkoutConfiguration = CheckoutConfiguration( Environment.TEST, - TEST_CLIENT_KEY + TEST_CLIENT_KEY, + ) { + dropIn { + setEnableRemovingStoredPaymentMethods(true) + } + } + + val dropInParams = DropInParamsMapper().mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = Locale.US, + sessionParams = null, ) - .setEnableRemovingStoredPaymentMethods(true) - .build() viewModel = PreselectedStoredPaymentViewModel( storedPaymentMethod, - TEST_AMOUNT, - dropInConfiguration, + dropInParams, ) viewModel.uiStateFlow.test { diff --git a/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/model/DropInParamsMapperTest.kt b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/model/DropInParamsMapperTest.kt new file mode 100644 index 0000000000..7b7179c765 --- /dev/null +++ b/drop-in/src/test/java/com/adyen/checkout/dropin/internal/ui/model/DropInParamsMapperTest.kt @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 12/2/2024. + */ + +package com.adyen.checkout.dropin.internal.ui.model + +import android.os.Bundle +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration +import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.core.Environment +import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.dropin.dropIn +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.Locale + +internal class DropInParamsMapperTest { + + private val dropInParamsMapper = DropInParamsMapper() + + @Test + fun `when created without specific drop-in or checkout configurations, then params should have default values`() { + val configuration = createCheckoutConfiguration() + + val params = dropInParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null) + + val expected = getDropInParams() + + assertEquals(expected, params) + } + + @Test + fun `when specific drop-in configurations are set then params should match these values`() { + val additionalData = Bundle().apply { + putString("key", "value") + } + val overridePaymentMethod = "TYPE" to "NAME" + val configuration = createCheckoutConfiguration { + setShowPreselectedStoredPaymentMethod(false) + setSkipListWhenSinglePaymentMethod(true) + setEnableRemovingStoredPaymentMethods(true) + setAdditionalDataForDropInService(additionalData) + overridePaymentMethodName(overridePaymentMethod.first, overridePaymentMethod.second) + } + + val params = dropInParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null) + + val expectedOverriddenPaymentMethodInformation = with(overridePaymentMethod) { + hashMapOf(first to DropInPaymentMethodInformation(second)) + } + val expected = getDropInParams( + showPreselectedStoredPaymentMethod = false, + skipListWhenSinglePaymentMethod = true, + isRemovingStoredPaymentMethodsEnabled = true, + additionalDataForDropInService = additionalData, + overriddenPaymentMethodInformation = expectedOverriddenPaymentMethodInformation, + ) + + assertEquals(expected, params) + } + + @Test + fun `when both checkout and drop-in configurations are set then params should match checkout configuration`() { + val configuration = CheckoutConfiguration( + shopperLocale = Locale.GERMAN, + environment = Environment.EUROPE, + clientKey = TEST_CLIENT_KEY_2, + amount = Amount( + currency = "EUR", + value = 49_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + dropIn { + setShopperLocale(Locale.US) + setAmount(Amount("USD", 12)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } + + val params = dropInParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null) + + val expected = getDropInParams( + shopperLocale = Locale.GERMAN, + environment = Environment.EUROPE, + clientKey = TEST_CLIENT_KEY_2, + analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), + amount = Amount( + currency = "EUR", + value = 49_00L, + ), + ) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("showRemovePaymentMethodButtonSource") + fun `showRemovePaymentMethodButton should match value set in sessions then configuration`( + configurationValue: Boolean, + sessionsValue: Boolean?, + expectedValue: Boolean, + ) { + val testConfiguration = createCheckoutConfiguration { + setEnableRemovingStoredPaymentMethods(configurationValue) + } + + val sessionParams = createSessionParams( + showRemovePaymentMethodButton = sessionsValue, + ) + + val params = dropInParamsMapper.mapToParams( + checkoutConfiguration = testConfiguration, + deviceLocale = DEVICE_LOCALE, + sessionParams = sessionParams, + ) + + val expected = getDropInParams(isRemovingStoredPaymentMethodsEnabled = expectedValue) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("amountSource") + fun `amount should match value set in sessions then configuration`( + configurationValue: Amount?, + sessionsValue: Amount?, + expectedValue: Amount?, + ) { + val testConfiguration = createCheckoutConfiguration(configurationValue) + + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val params = dropInParamsMapper.mapToParams( + checkoutConfiguration = testConfiguration, + deviceLocale = DEVICE_LOCALE, + sessionParams = sessionParams, + ) + + val expected = getDropInParams(amount = expectedValue) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = dropInParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + sessionParams = sessionParams, + ) + + val expected = getDropInParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = dropInParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + sessionParams = sessionParams, + ) + + val expected = getDropInParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configurationBlock: DropInConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + shopperLocale = shopperLocale, + amount = amount, + ) { + dropIn(configurationBlock) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, + ) + + @Suppress("LongParameterList") + private fun getDropInParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + shopperLocale: Locale = DEVICE_LOCALE, + analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + amount: Amount? = null, + showPreselectedStoredPaymentMethod: Boolean = true, + skipListWhenSinglePaymentMethod: Boolean = false, + isRemovingStoredPaymentMethodsEnabled: Boolean = false, + additionalDataForDropInService: Bundle? = null, + overriddenPaymentMethodInformation: Map = emptyMap(), + ): DropInParams { + return DropInParams( + environment = environment, + clientKey = clientKey, + shopperLocale = shopperLocale, + analyticsParams = analyticsParams, + amount = amount, + showPreselectedStoredPaymentMethod = showPreselectedStoredPaymentMethod, + skipListWhenSinglePaymentMethod = skipListWhenSinglePaymentMethod, + isRemovingStoredPaymentMethodsEnabled = isRemovingStoredPaymentMethodsEnabled, + additionalDataForDropInService = additionalDataForDropInService, + overriddenPaymentMethodInformation = overriddenPaymentMethodInformation, + ) + } + + companion object { + private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") + + @JvmStatic + fun showRemovePaymentMethodButtonSource() = listOf( + // configurationValue, sessionsValue, expectedValue + arguments(true, false, false), + arguments(true, null, true), + arguments(false, true, true), + arguments(false, null, false), + ) + + @JvmStatic + fun amountSource() = listOf( + // configurationValue, sessionsValue, expectedValue + arguments(Amount("EUR", 100), Amount("CAD", 300), Amount("CAD", 300)), + arguments(Amount("EUR", 100), null, Amount("EUR", 100)), + arguments(null, Amount("CAD", 300), Amount("CAD", 300)), + arguments(null, null, null), + ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) + } +} diff --git a/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextComponent.kt b/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextComponent.kt index d3c5e526c1..752d9ef2f9 100644 --- a/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextComponent.kt +++ b/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextComponent.kt @@ -23,8 +23,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.paymentmethod.EContextPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.econtext.internal.ui.EContextDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -72,12 +72,13 @@ abstract class EContextComponent< override fun isConfirmationRequired() = eContextDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? EContextDelegate<*, *>)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } internal fun removeObserver() { @@ -87,13 +88,9 @@ abstract class EContextComponent< override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } eContextDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextConfiguration.kt b/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextConfiguration.kt index 6d90d6fd0b..d1c70eed48 100644 --- a/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextConfiguration.kt +++ b/econtext/src/main/java/com/adyen/checkout/econtext/internal/EContextConfiguration.kt @@ -35,6 +35,22 @@ abstract class EContextConfiguration : Configuration, ButtonConfiguration { protected var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -42,6 +58,7 @@ abstract class EContextConfiguration : Configuration, ButtonConfiguration { * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, @@ -49,7 +66,7 @@ abstract class EContextConfiguration : Configuration, ButtonConfiguration { ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. diff --git a/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt b/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt index 39a2110b8c..d9dd909bda 100644 --- a/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt +++ b/econtext/src/main/java/com/adyen/checkout/econtext/internal/provider/EContextComponentProvider.kt @@ -17,6 +17,7 @@ import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentComponentData @@ -32,13 +33,14 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.components.core.paymentmethod.EContextPaymentMethod import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.econtext.internal.EContextComponent import com.adyen.checkout.econtext.internal.EContextConfiguration import com.adyen.checkout.econtext.internal.ui.DefaultEContextDelegate @@ -54,7 +56,7 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponen import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -@Suppress("ktlint:standard:type-parameter-list-spacing") +@Suppress("TooManyFunctions", "ktlint:standard:type-parameter-list-spacing") abstract class EContextComponentProvider< ComponentT : EContextComponent, ConfigurationT : EContextConfiguration, @@ -64,25 +66,23 @@ abstract class EContextComponentProvider< @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( private val componentClass: Class, - overrideComponentParams: ComponentParams?, - overrideSessionParams: SessionParams?, + private val dropInOverrideParams: DropInOverrideParams?, private val analyticsRepository: AnalyticsRepository?, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider>, SessionPaymentComponentProvider< ComponentT, ConfigurationT, ComponentStateT, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: ConfigurationT, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -92,7 +92,13 @@ constructor( val genericFactory: ViewModelProvider.Factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = getConfiguration(checkoutConfiguration), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -101,7 +107,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -113,11 +119,11 @@ constructor( analyticsRepository = analyticsRepository, submitHandler = SubmitHandler(savedStateHandle), typedPaymentMethodFactory = { createPaymentMethod() }, - componentStateFactory = ::createComponentState + componentStateFactory = ::createComponentState, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -126,7 +132,7 @@ constructor( delegate = eContextDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, eContextDelegate), - componentEventHandler = DefaultComponentEventHandler() + componentEventHandler = DefaultComponentEventHandler(), ) } return ViewModelProvider(viewModelStoreOwner, genericFactory)[key, componentClass].also { component -> @@ -136,6 +142,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: ConfigurationT, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): ComponentT { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = getCheckoutConfiguration(configuration), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -143,7 +173,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: ConfigurationT, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -152,10 +182,14 @@ constructor( val genericFactory: ViewModelProvider.Factory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = getConfiguration(checkoutConfiguration), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -166,7 +200,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -178,11 +212,11 @@ constructor( analyticsRepository = analyticsRepository, submitHandler = SubmitHandler(savedStateHandle), typedPaymentMethodFactory = { createPaymentMethod() }, - componentStateFactory = ::createComponentState + componentStateFactory = ::createComponentState, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -197,11 +231,11 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, - sessionSavedStateHandleContainer = sessionSavedStateHandleContainer + sessionSavedStateHandleContainer = sessionSavedStateHandleContainer, ) createComponent( @@ -211,6 +245,7 @@ constructor( componentEventHandler = sessionComponentEventHandler, ) } + return ViewModelProvider(viewModelStoreOwner, genericFactory)[key, componentClass].also { component -> component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, componentCallback) @@ -218,6 +253,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: ConfigurationT, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): ComponentT { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = getCheckoutConfiguration(configuration), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + protected abstract fun createComponentState( data: PaymentComponentData, isInputValid: Boolean, @@ -235,6 +294,10 @@ constructor( abstract fun getSupportedPaymentMethods(): List + protected abstract fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): ConfigurationT? + + protected abstract fun getCheckoutConfiguration(configuration: ConfigurationT): CheckoutConfiguration + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegate.kt b/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegate.kt index c7e1c1b9b1..d820f87b40 100644 --- a/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegate.kt +++ b/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegate.kt @@ -23,8 +23,8 @@ import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.ValidationUtils import com.adyen.checkout.components.core.paymentmethod.EContextPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.econtext.R import com.adyen.checkout.econtext.internal.ui.model.EContextInputData import com.adyen.checkout.econtext.internal.ui.model.EContextOutputData @@ -80,7 +80,7 @@ internal class DefaultEContextDelegate< } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -102,7 +102,7 @@ internal class DefaultEContextDelegate< firstNameState = validateFirstName(inputData.firstName), lastNameState = validateLastName(inputData.lastName), phoneNumberState = validatePhoneNumber(inputData.mobileNumber, inputData.countryCode), - emailAddressState = validateEmailAddress(inputData.emailAddress) + emailAddressState = validateEmailAddress(inputData.emailAddress), ) } @@ -213,8 +213,4 @@ internal class DefaultEContextDelegate< override fun setInteractionBlocked(isInteractionBlocked: Boolean) { submitHandler.setInteractionBlocked(isInteractionBlocked) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/view/EContextView.kt b/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/view/EContextView.kt index b3d2b56fa3..9cb4c47d41 100644 --- a/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/view/EContextView.kt +++ b/econtext/src/main/java/com/adyen/checkout/econtext/internal/ui/view/EContextView.kt @@ -27,6 +27,7 @@ import com.adyen.checkout.ui.core.internal.ui.model.CountryModel import com.adyen.checkout.ui.core.internal.ui.view.AdyenTextInputEditText import com.adyen.checkout.ui.core.internal.util.setLocalizedHintFromStyle import kotlinx.coroutines.CoroutineScope +import java.util.Locale @Suppress("TooManyFunctions") internal class EContextView @JvmOverloads constructor( @@ -175,7 +176,8 @@ internal class EContextView @JvmOverloads constructor( val country = countryAdapter?.getItem(position) ?: return@OnItemClickListener onCountrySelected(country) } - countries.firstOrNull()?.let { + val initialCountry = countries.firstOrNull { it.isoCode == Locale.JAPAN.country } ?: countries.firstOrNull() + initialCountry?.let { setText(it.toShortString()) onCountrySelected(it) } diff --git a/econtext/src/main/res/values-ar/strings.xml b/econtext/src/main/res/values-ar/strings.xml index 48dc646181..826c09ebdd 100644 --- a/econtext/src/main/res/values-ar/strings.xml +++ b/econtext/src/main/res/values-ar/strings.xml @@ -13,8 +13,8 @@ رقم الهاتف عنوان البريد الإلكتروني - الاسم الأول غير صحيح - الاسم الأخير غير صحيح + أدخل اسمك الأول + أدخل اسمك الأخير رقم هاتف غير صحيح عنوان بريد إلكتروني غير صحيح \ No newline at end of file diff --git a/econtext/src/main/res/values-cs-rCZ/strings.xml b/econtext/src/main/res/values-cs-rCZ/strings.xml index f933c1e8f8..a97401cb89 100644 --- a/econtext/src/main/res/values-cs-rCZ/strings.xml +++ b/econtext/src/main/res/values-cs-rCZ/strings.xml @@ -13,8 +13,8 @@ Telefonní číslo E-mailová adresa - Křestní jméno není platné - Příjmení není platné + Zadejte své křestní jméno + Zadejte své příjmení Neplatné telefonní číslo Neplatná e-mailová adresa \ No newline at end of file diff --git a/econtext/src/main/res/values-da-rDK/strings.xml b/econtext/src/main/res/values-da-rDK/strings.xml index ffd1a35568..0f47949e20 100644 --- a/econtext/src/main/res/values-da-rDK/strings.xml +++ b/econtext/src/main/res/values-da-rDK/strings.xml @@ -13,8 +13,8 @@ Telefonnummer E-mailadresse - Fornavnet er ikke gyldigt - Efternavnet er ikke gyldigt + Indtast dit fornavn + Indtast dit efternavn Ugyldigt telefonnummer Ugyldig e-mailadresse \ No newline at end of file diff --git a/econtext/src/main/res/values-de-rDE/strings.xml b/econtext/src/main/res/values-de-rDE/strings.xml index 4ff01bf1ff..a196f8d587 100644 --- a/econtext/src/main/res/values-de-rDE/strings.xml +++ b/econtext/src/main/res/values-de-rDE/strings.xml @@ -13,8 +13,8 @@ Telefonnummer E-Mail-Adresse - Vorname ist ungültig - Nachname ist ungültig + Geben Sie Ihren Vornamen ein + Geben Sie Ihren Nachnamen ein Ungültige Telefonnummer Ungültige E-Mail-Adresse \ No newline at end of file diff --git a/econtext/src/main/res/values-el-rGR/strings.xml b/econtext/src/main/res/values-el-rGR/strings.xml index f6561d272f..c62c128756 100644 --- a/econtext/src/main/res/values-el-rGR/strings.xml +++ b/econtext/src/main/res/values-el-rGR/strings.xml @@ -13,8 +13,8 @@ Αριθμός τηλεφώνου Διεύθυνση email - Το όνομα δεν είναι έγκυρο - Το επώνυμο δεν είναι έγκυρο + Πληκτρολογήστε το όνομά σας + Πληκτρολογήστε το επώνυμό σας Μη έγκυρος αριθμός τηλεφώνου Μη έγκυρη διεύθυνση email \ No newline at end of file diff --git a/econtext/src/main/res/values-es-rES/strings.xml b/econtext/src/main/res/values-es-rES/strings.xml index 6c10a32444..08d34204aa 100644 --- a/econtext/src/main/res/values-es-rES/strings.xml +++ b/econtext/src/main/res/values-es-rES/strings.xml @@ -13,8 +13,8 @@ Número de teléfono Dirección de correo electrónico - El nombre no es válido - El apellido no es válido + Introduzca su nombre + Introduzca su apellido El número de teléfono no es válido La dirección de correo electrónico no es válida \ No newline at end of file diff --git a/econtext/src/main/res/values-fi-rFI/strings.xml b/econtext/src/main/res/values-fi-rFI/strings.xml index 33413a39ef..b656832efc 100644 --- a/econtext/src/main/res/values-fi-rFI/strings.xml +++ b/econtext/src/main/res/values-fi-rFI/strings.xml @@ -13,8 +13,8 @@ Puhelinnumero Sähköpostiosoite - Etunimi ei ole kelvollinen - Sukunimi ei ole kelvollinen + Syötä etunimesi + Syötä sukunimesi Ei-kelvollinen puhelinnumero Ei-kelvollinen sähköpostiosoite \ No newline at end of file diff --git a/econtext/src/main/res/values-fr-rFR/strings.xml b/econtext/src/main/res/values-fr-rFR/strings.xml index d808161d5e..5f8cdecf73 100644 --- a/econtext/src/main/res/values-fr-rFR/strings.xml +++ b/econtext/src/main/res/values-fr-rFR/strings.xml @@ -13,8 +13,8 @@ Numéro de téléphone Adresse e-mail - Le prénom n\'est pas valide - Le nom n\'est pas valide + Entrez votre prénom + Entrez votre nom Numéro de téléphone incorrect Adresse e-mail incorrecte \ No newline at end of file diff --git a/econtext/src/main/res/values-hr-rHR/strings.xml b/econtext/src/main/res/values-hr-rHR/strings.xml index a3a5ef3e36..0ce92b1e96 100644 --- a/econtext/src/main/res/values-hr-rHR/strings.xml +++ b/econtext/src/main/res/values-hr-rHR/strings.xml @@ -13,8 +13,8 @@ Telefonski broj Adresa e-pošte - Ime nije valjano - Prezime nije valjano + Unesite svoje ime + Unesite svoje prezime Nevažeći telefonski broj Nevažeća adresa e-pošte \ No newline at end of file diff --git a/econtext/src/main/res/values-hu-rHU/strings.xml b/econtext/src/main/res/values-hu-rHU/strings.xml index 53986e493c..5d7854879b 100644 --- a/econtext/src/main/res/values-hu-rHU/strings.xml +++ b/econtext/src/main/res/values-hu-rHU/strings.xml @@ -13,8 +13,8 @@ Telefonszám E-mail-cím - A keresztnév nem érvényes - A vezetéknév nem érvényes + Adja meg a keresztnevét + Adja meg a vezetéknevét Érvénytelen telefonszám Érvénytelen e-mail-cím \ No newline at end of file diff --git a/econtext/src/main/res/values-it-rIT/strings.xml b/econtext/src/main/res/values-it-rIT/strings.xml index 97c6660592..b7fda79faa 100644 --- a/econtext/src/main/res/values-it-rIT/strings.xml +++ b/econtext/src/main/res/values-it-rIT/strings.xml @@ -13,8 +13,8 @@ Numero di telefono Indirizzo e-mail - Nome non valido - Cognome non valido + Immetti il tuo nome + Immetti il tuo cognome Numero di telefono non valido Indirizzo e-mail non valido \ No newline at end of file diff --git a/econtext/src/main/res/values-ja-rJP/strings.xml b/econtext/src/main/res/values-ja-rJP/strings.xml index b5d088e59b..3b6e79b2ce 100644 --- a/econtext/src/main/res/values-ja-rJP/strings.xml +++ b/econtext/src/main/res/values-ja-rJP/strings.xml @@ -13,8 +13,8 @@ 電話番号 Eメールアドレス - 名が無効です - 姓が無効です + 名を入力してください + 姓を入力してください 無効な電話番号 Eメールアドレスが無効です \ No newline at end of file diff --git a/econtext/src/main/res/values-ko-rKR/strings.xml b/econtext/src/main/res/values-ko-rKR/strings.xml index 0bdeb9cf09..2f12cd66f1 100644 --- a/econtext/src/main/res/values-ko-rKR/strings.xml +++ b/econtext/src/main/res/values-ko-rKR/strings.xml @@ -13,8 +13,8 @@ 전화번호 이메일 주소 - 이름이 올바르지 않습니다 - 성이 올바르지 않습니다 + 이름을 입력하세요. + 성을 입력하세요. 유효하지 않은 전화번호 유효하지 않은 이메일 주소 \ No newline at end of file diff --git a/econtext/src/main/res/values-nb-rNO/strings.xml b/econtext/src/main/res/values-nb-rNO/strings.xml index d7e534bbdb..c29af8e544 100644 --- a/econtext/src/main/res/values-nb-rNO/strings.xml +++ b/econtext/src/main/res/values-nb-rNO/strings.xml @@ -13,8 +13,8 @@ Telefonnummer E-postadresse - Fornavnet er ikke gyldig - Etternavnet er ikke gyldig + Skriv inn fornavnet ditt + Skriv inn etternavnet ditt Ugyldig telefonnummer Ugyldig e-postadresse \ No newline at end of file diff --git a/econtext/src/main/res/values-nl-rNL/strings.xml b/econtext/src/main/res/values-nl-rNL/strings.xml index 854d515182..4977cf224f 100644 --- a/econtext/src/main/res/values-nl-rNL/strings.xml +++ b/econtext/src/main/res/values-nl-rNL/strings.xml @@ -13,8 +13,8 @@ Telefoonnummer E-mailadres - Voornaam is niet geldig - Achternaam is niet geldig + Voer je voornaam in + Voer je achternaam in Ongeldig telefoonnummer Ongeldig e-mailadres \ No newline at end of file diff --git a/econtext/src/main/res/values-pl-rPL/strings.xml b/econtext/src/main/res/values-pl-rPL/strings.xml index 1d0b460ecc..b21125bc78 100644 --- a/econtext/src/main/res/values-pl-rPL/strings.xml +++ b/econtext/src/main/res/values-pl-rPL/strings.xml @@ -13,8 +13,8 @@ Numer telefonu Adres e-mail - Imię jest nieprawidłowe - Nazwisko jest nieprawidłowe + Wpisz imię + Wpisz nazwisko Nieprawidłowy numer telefonu Niepoprawny adres email \ No newline at end of file diff --git a/econtext/src/main/res/values-pt-rBR/strings.xml b/econtext/src/main/res/values-pt-rBR/strings.xml index d6cf4d8e6f..fbbb42bb47 100644 --- a/econtext/src/main/res/values-pt-rBR/strings.xml +++ b/econtext/src/main/res/values-pt-rBR/strings.xml @@ -13,8 +13,8 @@ Número de telefone Endereço de e-mail - Este não é um nome válido - Este não é um sobrenome válido + Digite seu nome + Digite seu sobrenome Número de telefone inválido Endereço de e-mail inválido \ No newline at end of file diff --git a/econtext/src/main/res/values-pt-rPT/strings.xml b/econtext/src/main/res/values-pt-rPT/strings.xml index 97f9031a2f..88a9c17df2 100644 --- a/econtext/src/main/res/values-pt-rPT/strings.xml +++ b/econtext/src/main/res/values-pt-rPT/strings.xml @@ -13,8 +13,8 @@ Número de telefone Endereço de correio eletrónico - O nome próprio não é válido - O apelido não é válido + Introduza o seu nome próprio + Introduza o seu apelido Número de telefone inválido Endereço de e-mail inválido \ No newline at end of file diff --git a/econtext/src/main/res/values-ro-rRO/strings.xml b/econtext/src/main/res/values-ro-rRO/strings.xml index acd490458d..401d635864 100644 --- a/econtext/src/main/res/values-ro-rRO/strings.xml +++ b/econtext/src/main/res/values-ro-rRO/strings.xml @@ -13,8 +13,8 @@ Număr de telefon Adresă de e-mail - Prenumele nu este valabil - Numele de familie nu este valabil + Completați prenumele dvs. + Completați numele dvs. de familie Număr de telefon incorect Adresă de e-mail incorectă \ No newline at end of file diff --git a/econtext/src/main/res/values-ru-rRU/strings.xml b/econtext/src/main/res/values-ru-rRU/strings.xml index 4d0c38db8d..b23ded6c3e 100644 --- a/econtext/src/main/res/values-ru-rRU/strings.xml +++ b/econtext/src/main/res/values-ru-rRU/strings.xml @@ -13,8 +13,8 @@ Номер телефона Адрес эл. почты - Неверное имя - Неверная фамилия + Введите имя + Введите фамилию Недействительный номер телефона Недействительный адрес эл. почты \ No newline at end of file diff --git a/econtext/src/main/res/values-sk-rSK/strings.xml b/econtext/src/main/res/values-sk-rSK/strings.xml index 4d3180fd89..60823dce0b 100644 --- a/econtext/src/main/res/values-sk-rSK/strings.xml +++ b/econtext/src/main/res/values-sk-rSK/strings.xml @@ -13,8 +13,8 @@ Telefónne číslo E-mailová adresa - Meno nie je platné - Priezvisko nie je platné + Zadajte svoje meno + Zadajte svoje priezvisko Neplatné telefónne číslo Neplatná emailová adresa \ No newline at end of file diff --git a/econtext/src/main/res/values-sl-rSI/strings.xml b/econtext/src/main/res/values-sl-rSI/strings.xml index 77c98bb9cb..bb19170ad1 100644 --- a/econtext/src/main/res/values-sl-rSI/strings.xml +++ b/econtext/src/main/res/values-sl-rSI/strings.xml @@ -13,8 +13,8 @@ Telefonska številka Elektronski naslov - Ime ni veljavno - Priimek ni veljaven + Vnesite svoje ime + Vnesite svoj priimek Neveljavna telefonska številka Neveljaven elektronski naslov \ No newline at end of file diff --git a/econtext/src/main/res/values-sv-rSE/strings.xml b/econtext/src/main/res/values-sv-rSE/strings.xml index 5052eb5364..af0ef0a011 100644 --- a/econtext/src/main/res/values-sv-rSE/strings.xml +++ b/econtext/src/main/res/values-sv-rSE/strings.xml @@ -13,8 +13,8 @@ Telefonnummer E-postadress - Förnamnet är inte giltigt - Efternamnet är inte giltigt + Ange ditt förnamn + Ange ditt efternamn Ogiltigt telefonnummer Ogiltig e-postadress \ No newline at end of file diff --git a/econtext/src/main/res/values-zh-rCN/strings.xml b/econtext/src/main/res/values-zh-rCN/strings.xml index d84a2a291c..2200ba8307 100644 --- a/econtext/src/main/res/values-zh-rCN/strings.xml +++ b/econtext/src/main/res/values-zh-rCN/strings.xml @@ -13,8 +13,8 @@ 电话号码 电子邮件地址 - 名字无效 - 姓氏无效 + 输入您的名字 + 输入您的姓氏 无效的电话号码 无效的邮件地址 \ No newline at end of file diff --git a/econtext/src/main/res/values-zh-rTW/strings.xml b/econtext/src/main/res/values-zh-rTW/strings.xml index 3822d769dd..3b2ad6d4cd 100644 --- a/econtext/src/main/res/values-zh-rTW/strings.xml +++ b/econtext/src/main/res/values-zh-rTW/strings.xml @@ -13,8 +13,8 @@ 電話號碼 電子郵件地址 - 名字無效 - 姓氏無效 + 輸入您的名字 + 輸入您的姓氏 電話號碼無效 電子郵件地址無效 \ No newline at end of file diff --git a/econtext/src/main/res/values/strings.xml b/econtext/src/main/res/values/strings.xml index 9281c7b23c..e805212058 100644 --- a/econtext/src/main/res/values/strings.xml +++ b/econtext/src/main/res/values/strings.xml @@ -13,8 +13,8 @@ Telephone number Email address - First name is not valid - Last name is not valid + Enter your first name + Enter your last name Invalid telephone number Invalid email address \ No newline at end of file diff --git a/econtext/src/test/java/com/adyen/checkout/econtext/TestEContextConfiguration.kt b/econtext/src/test/java/com/adyen/checkout/econtext/TestEContextConfiguration.kt index 5bd298f6e5..89ebe613d2 100644 --- a/econtext/src/test/java/com/adyen/checkout/econtext/TestEContextConfiguration.kt +++ b/econtext/src/test/java/com/adyen/checkout/econtext/TestEContextConfiguration.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.econtext -import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration @@ -22,7 +21,7 @@ internal class TestEContextConfiguration @Suppress("LongParameterList") private constructor( override val isSubmitButtonVisible: Boolean?, - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -30,19 +29,12 @@ private constructor( override val genericActionConfiguration: GenericActionConfiguration ) : EContextConfiguration() { - class Builder : EContextConfiguration.Builder { + class Builder(shopperLocale: Locale?, environment: Environment, clientKey: String) : + EContextConfiguration.Builder(environment, clientKey) { - constructor(context: Context, environment: Environment, clientKey: String) : super( - context, - environment, - clientKey - ) - - constructor( - shopperLocale: Locale, - environment: Environment, - clientKey: String - ) : super(shopperLocale, environment, clientKey) + init { + shopperLocale?.let { setShopperLocale(it) } + } public override fun buildInternal(): TestEContextConfiguration { return TestEContextConfiguration( diff --git a/econtext/src/test/java/com/adyen/checkout/econtext/internal/EContextComponentTest.kt b/econtext/src/test/java/com/adyen/checkout/econtext/internal/EContextComponentTest.kt index 3733df9aa8..9265acc48a 100644 --- a/econtext/src/test/java/com/adyen/checkout/econtext/internal/EContextComponentTest.kt +++ b/econtext/src/test/java/com/adyen/checkout/econtext/internal/EContextComponentTest.kt @@ -15,17 +15,15 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.econtext.TestEContextComponent import com.adyen.checkout.econtext.TestEContextComponentState import com.adyen.checkout.econtext.TestEContextPaymentMethod import com.adyen.checkout.econtext.internal.ui.EContextComponentViewType import com.adyen.checkout.econtext.internal.ui.EContextDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions @@ -42,8 +40,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class EContextComponentTest( @Mock private val eContextDelegate: EContextDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -66,7 +63,6 @@ internal class EContextComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/econtext/src/test/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegateTest.kt b/econtext/src/test/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegateTest.kt index 3a4d013595..d28cdfa034 100644 --- a/econtext/src/test/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegateTest.kt +++ b/econtext/src/test/java/com/adyen/checkout/econtext/internal/ui/DefaultEContextDelegateTest.kt @@ -10,21 +10,22 @@ package com.adyen.checkout.econtext.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.econtext.TestEContextComponentState import com.adyen.checkout.econtext.TestEContextConfiguration import com.adyen.checkout.econtext.TestEContextPaymentMethod import com.adyen.checkout.econtext.internal.ui.model.EContextOutputData +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,10 +47,10 @@ import org.mockito.Mockito.verify import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.doReturn import org.mockito.kotlin.whenever -import java.util.* +import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultEContextDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, @Mock private val submitHandler: SubmitHandler, @@ -60,7 +61,6 @@ internal class DefaultEContextDelegateTest( @BeforeEach fun beforeEach() { delegate = createEContextDelegate() - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -139,7 +139,7 @@ internal class DefaultEContextDelegateTest( lastNameState = FieldState("", Validation.Invalid(0)), phoneNumberState = FieldState("", Validation.Invalid(0)), emailAddressState = FieldState("", Validation.Invalid(0)), - ) + ), ) with(expectMostRecentItem()) { assertFalse(isInputValid) @@ -157,7 +157,7 @@ internal class DefaultEContextDelegateTest( lastNameState = FieldState("lastName", Validation.Valid), phoneNumberState = FieldState("phoneNumber", Validation.Valid), emailAddressState = FieldState("emailAddress", Validation.Valid), - ) + ), ) with(expectMostRecentItem()) { with(requireNotNull(data.paymentMethod)) { @@ -180,9 +180,7 @@ internal class DefaultEContextDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultTestEContextConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createEContextDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -211,9 +209,9 @@ internal class DefaultEContextDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createEContextDelegate( - configuration = getDefaultTestEContextConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -222,9 +220,9 @@ internal class DefaultEContextDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createEContextDelegate( - configuration = getDefaultTestEContextConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -282,11 +280,17 @@ internal class DefaultEContextDelegateTest( } private fun createEContextDelegate( - configuration: TestEContextConfiguration = getDefaultTestEContextConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: Order = TEST_ORDER ) = DefaultEContextDelegate( observerRepository = PaymentObserverRepository(), - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + ), paymentMethod = PaymentMethod(), order = order, analyticsRepository = analyticsRepository, @@ -296,21 +300,31 @@ internal class DefaultEContextDelegateTest( TestEContextComponentState( data = data, isInputValid = isInputValid, - isReady = isReady + isReady = isReady, ) - } + }, ) - private fun getDefaultTestEContextConfigurationBuilder() = TestEContextConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY_1 - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: TestEContextConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + val econtextConfiguration = TestEContextConfiguration.Builder(shopperLocale, environment, clientKey) + .apply(configuration) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, econtextConfiguration) + } companion object { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") private const val TEST_CHECKOUT_ATTEMPT_ID = "TEST_CHECKOUT_ATTEMPT_ID" + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" @JvmStatic fun amountSource() = listOf( diff --git a/entercash/src/main/java/com/adyen/checkout/entercash/EntercashConfiguration.kt b/entercash/src/main/java/com/adyen/checkout/entercash/EntercashConfiguration.kt index 1d76d2f743..1a6d5e4d07 100644 --- a/entercash/src/main/java/com/adyen/checkout/entercash/EntercashConfiguration.kt +++ b/entercash/src/main/java/com/adyen/checkout/entercash/EntercashConfiguration.kt @@ -11,6 +11,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -23,7 +26,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class EntercashConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -39,6 +42,22 @@ class EntercashConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -46,14 +65,15 @@ class EntercashConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -80,3 +100,38 @@ class EntercashConfiguration private constructor( } } } + +fun CheckoutConfiguration.entercash( + configuration: @CheckoutConfigurationMarker EntercashConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = EntercashConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ENTERCASH, config) + return this +} + +fun CheckoutConfiguration.getEntercashConfiguration(): EntercashConfiguration? { + return getConfiguration(PaymentMethodTypes.ENTERCASH) +} + +internal fun EntercashConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ENTERCASH, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/entercash/src/main/java/com/adyen/checkout/entercash/internal/provider/EntercashComponentProvider.kt b/entercash/src/main/java/com/adyen/checkout/entercash/internal/provider/EntercashComponentProvider.kt index 4a6f3ab984..844cbdf7ed 100644 --- a/entercash/src/main/java/com/adyen/checkout/entercash/internal/provider/EntercashComponentProvider.kt +++ b/entercash/src/main/java/com/adyen/checkout/entercash/internal/provider/EntercashComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.entercash.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.EntercashPaymentMethod import com.adyen.checkout.entercash.EntercashComponent import com.adyen.checkout.entercash.EntercashComponentState import com.adyen.checkout.entercash.EntercashConfiguration +import com.adyen.checkout.entercash.getEntercashConfiguration +import com.adyen.checkout.entercash.toCheckoutConfiguration import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate class EntercashComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider< EntercashComponent, EntercashConfiguration, EntercashPaymentMethod, - EntercashComponentState + EntercashComponentState, >( componentClass = EntercashComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -62,4 +62,12 @@ constructor( override fun createPaymentMethod() = EntercashPaymentMethod() override fun getSupportedPaymentMethods(): List = EntercashComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): EntercashConfiguration? { + return checkoutConfiguration.getEntercashConfiguration() + } + + override fun getCheckoutConfiguration(configuration: EntercashConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/entercash/src/test/java/com/adyen/checkout/entercash/EntercashConfigurationTest.kt b/entercash/src/test/java/com/adyen/checkout/entercash/EntercashConfigurationTest.kt new file mode 100644 index 0000000000..007083bc58 --- /dev/null +++ b/entercash/src/test/java/com/adyen/checkout/entercash/EntercashConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.entercash + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class EntercashConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + entercash { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getEntercashConfiguration() + + val expected = EntercashConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = EntercashConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualEnterConfig = actual.getEntercashConfiguration() + assertEquals(config.shopperLocale, actualEnterConfig?.shopperLocale) + assertEquals(config.environment, actualEnterConfig?.environment) + assertEquals(config.clientKey, actualEnterConfig?.clientKey) + assertEquals(config.amount, actualEnterConfig?.amount) + assertEquals(config.analyticsConfiguration, actualEnterConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualEnterConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualEnterConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualEnterConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/eps/src/main/java/com/adyen/checkout/eps/EPSConfiguration.kt b/eps/src/main/java/com/adyen/checkout/eps/EPSConfiguration.kt index ef16d489d2..8034628d7d 100644 --- a/eps/src/main/java/com/adyen/checkout/eps/EPSConfiguration.kt +++ b/eps/src/main/java/com/adyen/checkout/eps/EPSConfiguration.kt @@ -11,6 +11,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -23,7 +26,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class EPSConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -39,6 +42,22 @@ class EPSConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -46,10 +65,11 @@ class EPSConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** @@ -64,7 +84,7 @@ class EPSConfiguration private constructor( } /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -91,3 +111,38 @@ class EPSConfiguration private constructor( } } } + +fun CheckoutConfiguration.eps( + configuration: @CheckoutConfigurationMarker EPSConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = EPSConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.EPS, config) + return this +} + +fun CheckoutConfiguration.getEPSConfiguration(): EPSConfiguration? { + return getConfiguration(PaymentMethodTypes.EPS) +} + +internal fun EPSConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.EPS, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/eps/src/main/java/com/adyen/checkout/eps/internal/provider/EPSComponentProvider.kt b/eps/src/main/java/com/adyen/checkout/eps/internal/provider/EPSComponentProvider.kt index e5b5f50ece..f87ed9ccee 100644 --- a/eps/src/main/java/com/adyen/checkout/eps/internal/provider/EPSComponentProvider.kt +++ b/eps/src/main/java/com/adyen/checkout/eps/internal/provider/EPSComponentProvider.kt @@ -11,30 +11,30 @@ package com.adyen.checkout.eps.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.EPSPaymentMethod import com.adyen.checkout.eps.EPSComponent import com.adyen.checkout.eps.EPSComponentState import com.adyen.checkout.eps.EPSConfiguration +import com.adyen.checkout.eps.getEPSConfiguration +import com.adyen.checkout.eps.toCheckoutConfiguration import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate class EPSComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider( componentClass = EPSComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, - hideIssuerLogosDefaultValue = true + hideIssuerLogosDefaultValue = true, ) { override fun createComponent( @@ -58,4 +58,12 @@ constructor( override fun createPaymentMethod() = EPSPaymentMethod() override fun getSupportedPaymentMethods(): List = EPSComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): EPSConfiguration? { + return checkoutConfiguration.getEPSConfiguration() + } + + override fun getCheckoutConfiguration(configuration: EPSConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/eps/src/test/java/com/adyen/checkout/eps/EPSConfigurationTest.kt b/eps/src/test/java/com/adyen/checkout/eps/EPSConfigurationTest.kt new file mode 100644 index 0000000000..cf729dda05 --- /dev/null +++ b/eps/src/test/java/com/adyen/checkout/eps/EPSConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.eps + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class EPSConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + eps { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getEPSConfiguration() + + val expected = EPSConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = EPSConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualEpsConfig = actual.getEPSConfiguration() + assertEquals(config.shopperLocale, actualEpsConfig?.shopperLocale) + assertEquals(config.environment, actualEpsConfig?.environment) + assertEquals(config.clientKey, actualEpsConfig?.clientKey) + assertEquals(config.amount, actualEpsConfig?.amount) + assertEquals(config.analyticsConfiguration, actualEpsConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualEpsConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualEpsConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualEpsConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/example-app/README.md b/example-app/README.md new file mode 100644 index 0000000000..345c63032f --- /dev/null +++ b/example-app/README.md @@ -0,0 +1,27 @@ +# Example app + +The `example-app` module is used for development and testing purposes. It should not be used as a template for your own integration. Check out the [docs](https://docs.adyen.com/online-payments/build-your-integration/) for best practices on integration. + +## Running the app + +Steps to run the example app: +1. Build a server that acts as a proxy between the app and the Adyen Checkout API. + * Your server should mirror the necessary endpoints for your flow (for example [/sessions](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) for the sessions flow and [/paymentMethods](https://docs.adyen.com/api-explorer/Checkout/latest/post/paymentMethods), [/payments](https://docs.adyen.com/api-explorer/Checkout/latest/post/payments) and [/payments/details](https://docs.adyen.com/api-explorer/Checkout/latest/post/payments/details) for the advanced flow). + * The API key should be managed on the server. +2. Duplicate `example.local.gradle` and name it `local.gradle`. Make sure the file is placed in the `example-app` directory. +3. Replace the predefined values: + * `MERCHANT_SERVER_URL`: the URL to your server. + * `CLIENT_KEY`: your client key. Find out how to obtain it [here](https://docs.adyen.com/development-resources/client-side-authentication/#get-your-client-key). + * `MERCHANT_ACCOUNT`: your merchant account identifier. + * `AUTHORIZATION_HEADER_NAME`: the name of the authorization header as expected by your server. You can use an empty string if this is not applicable for you. + * `AUTHORIZATION_HEADER_VALUE`: the value for the authorization header. You can use an empty string if this is not applicable for you. +4. Sync the project. +5. Run on any device or emulator. + +> [!WARNING] +> In case you don't have your own server you can connect to the Adyen Checkout API directly for testing purposes only. Be aware this could potentially leak your credentials, the market-ready application must never connect to Adyen API directly. + +To connect to the Adyen Checkout API directly you can use the following values: +* `MERCHANT_SERVER_URL`: `https://checkout-test.adyen.com/{VERSION}/` (check [here](https://docs.adyen.com/api-explorer/Checkout/latest/overview) for the latest version). +* `AUTHORIZATION_HEADER_NAME`: `x-api-key`. +* `AUTHORIZATION_HEADER_VALUE`: your API key. diff --git a/example-app/build.gradle b/example-app/build.gradle index 6a523c3a96..7d8c258ece 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -18,9 +18,11 @@ apply from: "${rootDir}/config/gradle/ci.gradle" if (file("local.gradle").exists()) { apply from: "local.gradle" +} else if (System.getenv('CI')) { + // if building from CI use example file as it is to ensure the build passes + apply from: "example.local.gradle" } else { - logger.lifecycle("File example-app/local.gradle not found. Falling back to default file with no values.") - apply from: "default.local.gradle" + throw new GradleException("File example-app/local.gradle not found. Check example-app/README.md for more instructions.") } // This runConnectedAndroidTest.gradle script is applied, @@ -68,7 +70,7 @@ dependencies { // Checkout implementation project(':drop-in') implementation project(':components-compose') -// implementation "com.adyen.checkout:drop-in:5.2.0" +// implementation "com.adyen.checkout:drop-in:5.3.0" // Dependencies implementation libraries.kotlinCoroutines diff --git a/example-app/default.local.gradle b/example-app/default.local.gradle deleted file mode 100644 index 34679f12c5..0000000000 --- a/example-app/default.local.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Replace the values in with the configuration to connect to YOUR Server. The <> should also be removed. - * Your server is the one that should connect to Adyen, DO NOT connect to Adyen directly from the App as this might expose your API keys. - * - * For a permanent local configuration file not tracked by Git, copy this file and rename it to "local.gradle" with your credentials. - */ -android { - buildTypes { - debug { - buildConfigField "String", "MERCHANT_ACCOUNT", "\"\"" - buildConfigField "String", "MERCHANT_SERVER_URL", "\"\"" - buildConfigField "String", "API_KEY_HEADER_NAME", "\"\"" - buildConfigField "String", "CHECKOUT_API_KEY", "\"\"" - buildConfigField "String", "CLIENT_KEY", "\"\"" - buildConfigField "String", "SHOPPER_REFERENCE", "\"\"" - } - - release { - initWith debug - matchingFallbacks = ['debug'] - } - } -} diff --git a/example-app/example.local.gradle b/example-app/example.local.gradle new file mode 100644 index 0000000000..91799d1d88 --- /dev/null +++ b/example-app/example.local.gradle @@ -0,0 +1,22 @@ +/** + * Duplicate this file into "local.gradle" then replace the placeholder values with the correct + * parameters. You might need to escape some characters if you see an error. + * + * DO NOT commit the new file anywhere public, you might be exposing your secret credentials. + */ +android { + buildTypes { + debug { + buildConfigField "String", "MERCHANT_SERVER_URL", '"YOUR_SERVER_URL"' + buildConfigField "String", "CLIENT_KEY", '"YOUR_CLIENT_KEY"' + buildConfigField "String", "MERCHANT_ACCOUNT", '"YOUR_MERCHANT_ACCOUNT"' + buildConfigField "String", "AUTHORIZATION_HEADER_NAME", '"YOUR_AUTHORIZATION_HEADER_NAME"' + buildConfigField "String", "AUTHORIZATION_HEADER_VALUE", '"YOUR_AUTHORIZATION_HEADER_VALUE"' + } + + release { + initWith debug + matchingFallbacks = ['debug'] + } + } +} diff --git a/example-app/src/main/assets/lookup_options.json b/example-app/src/main/assets/lookup_options.json new file mode 100644 index 0000000000..362e3fd303 --- /dev/null +++ b/example-app/src/main/assets/lookup_options.json @@ -0,0 +1,70 @@ +{ + "options": [ + { + "id": "0", + "address": { + "country": "NL", + "postalCode": "1234AB", + "houseNumberOrName": "1HS", + "street": "Simon Carmiggelstraat", + "stateOrProvince": "Noord-Holland", + "city": "Amsterdam" + } + }, + { + "id": "1", + "address": { + "country": "TR", + "postalCode": "12345", + "houseNumberOrName": "1", + "street": "1. Sokak", + "stateOrProvince": "Istanbul", + "city": "Istanbul" + } + }, + { + "id": "2", + "address": { + "country": "DE", + "postalCode": "10119", + "houseNumberOrName": "1", + "street": "1. Strasse", + "stateOrProvince": "Berlin", + "city": "Berlin" + } + }, + { + "id": "3", + "address": { + "country": "US", + "postalCode": "91101", + "houseNumberOrName": "2222", + "street": "1. Avenue", + "stateOrProvince": "NY", + "city": "New York" + } + }, + { + "id": "4", + "address": { + "country": "BR", + "postalCode": "26363-260", + "houseNumberOrName": "2222", + "street": "Rua Mineiros 1594", + "stateOrProvince": "RJ", + "city": "Queimados" + } + }, + { + "id": "error", + "address": { + "country": "", + "postalCode": "", + "houseNumberOrName": "", + "street": "Error Option", + "stateOrProvince": "", + "city": "" + } + } + ] +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt b/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt index 8dcf2b87c6..5655946273 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/CheckoutExampleApplication.kt @@ -9,7 +9,7 @@ package com.adyen.checkout.example import android.app.Application -import android.util.Log +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.example.ui.theme.NightThemeRepository import dagger.hilt.android.HiltAndroidApp @@ -22,7 +22,7 @@ class CheckoutExampleApplication : Application() { internal lateinit var nightThemeRepository: NightThemeRepository init { - AdyenLogger.setLogLevel(Log.VERBOSE) + AdyenLogger.setLogLevel(AdyenLogLevel.VERBOSE) } override fun onCreate() { diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt index 40fe6f11ff..8e694ba992 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt @@ -9,7 +9,6 @@ package com.adyen.checkout.example.data.api import com.adyen.checkout.components.core.PaymentMethodsApiResponse -import com.adyen.checkout.example.BuildConfig import com.adyen.checkout.example.data.api.model.BalanceRequest import com.adyen.checkout.example.data.api.model.CancelOrderRequest import com.adyen.checkout.example.data.api.model.CreateOrderRequest @@ -27,14 +26,6 @@ import retrofit2.http.Query internal interface CheckoutApiService { - companion object { - private const val DEFAULT_GRADLE_SERVER_URL = "" - - fun isRealUrlAvailable(): Boolean { - return BuildConfig.MERCHANT_SERVER_URL != DEFAULT_GRADLE_SERVER_URL - } - } - @POST("sessions") suspend fun sessionsAsync(@Body sessionRequest: SessionRequest): SessionModel diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/ThreeDS2RequestDataRequest.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/AuthenticationData.kt similarity index 50% rename from example-app/src/main/java/com/adyen/checkout/example/data/api/model/ThreeDS2RequestDataRequest.kt rename to example-app/src/main/java/com/adyen/checkout/example/data/api/model/AuthenticationData.kt index add6f4c04a..f448ea4183 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/ThreeDS2RequestDataRequest.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/AuthenticationData.kt @@ -1,16 +1,17 @@ /* - * Copyright (c) 2022 Adyen N.V. + * Copyright (c) 2024 Adyen N.V. * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by josephj on 30/3/2022. + * Created by josephj on 29/2/2024. */ + package com.adyen.checkout.example.data.api.model import androidx.annotation.Keep @Keep -data class ThreeDS2RequestDataRequest( - val deviceChannel: String = "app", - val challengeIndicator: String = "requestChallenge" +data class AuthenticationData( + val attemptAuthentication: String, + val threeDSRequestData: ThreeDSRequestData?, ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentMethodsRequest.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentMethodsRequest.kt index 2089f3e291..08d582ec4c 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentMethodsRequest.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentMethodsRequest.kt @@ -18,7 +18,7 @@ data class PaymentMethodsRequest( val shopperReference: String, val amount: Amount?, val countryCode: String, - val shopperLocale: String, + val shopperLocale: String?, val channel: String, val splitCardFundingSources: Boolean, val order: OrderRequest?, diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentsRequest.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentsRequest.kt index 49a53541b3..716170b1e1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentsRequest.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/PaymentsRequest.kt @@ -30,13 +30,12 @@ data class PaymentsRequestData( val countryCode: String, val merchantAccount: String, val returnUrl: String, - val additionalData: AdditionalData, + val authenticationData: AuthenticationData, val threeDSAuthenticationOnly: Boolean, val shopperIP: String, val reference: String, val channel: String, val lineItems: List, val shopperEmail: String? = null, - val threeDS2RequestData: ThreeDS2RequestDataRequest?, val recurringProcessingModel: String? ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt index ff1855f4c7..0f22d6cbc8 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/SessionRequest.kt @@ -18,20 +18,20 @@ data class SessionRequest( val shopperReference: String, val amount: Amount?, val countryCode: String, - val shopperLocale: String, + val shopperLocale: String?, val channel: String, val splitCardFundingSources: Boolean, val returnUrl: String, - val additionalData: AdditionalData, + val authenticationData: AuthenticationData, val threeDSAuthenticationOnly: Boolean, val shopperIP: String, val reference: String, val lineItems: List, - val threeDS2RequestData: ThreeDS2RequestDataRequest?, val shopperEmail: String?, val allowedPaymentMethods: List?, val storePaymentMethodMode: String?, val recurringProcessingModel: String?, val installmentOptions: Map?, - val showInstallmentAmount: Boolean + val showInstallmentAmount: Boolean, + val showRemovePaymentMethodButton: Boolean, ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/AdditionalData.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/ThreeDSRequestData.kt similarity index 54% rename from example-app/src/main/java/com/adyen/checkout/example/data/api/model/AdditionalData.kt rename to example-app/src/main/java/com/adyen/checkout/example/data/api/model/ThreeDSRequestData.kt index 70cf02cc0b..31971e4b6d 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/model/AdditionalData.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/model/ThreeDSRequestData.kt @@ -1,9 +1,9 @@ /* - * Copyright (c) 2019 Adyen N.V. + * Copyright (c) 2024 Adyen N.V. * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by arman on 10/10/2019. + * Created by josephj on 29/2/2024. */ package com.adyen.checkout.example.data.api.model @@ -11,7 +11,6 @@ package com.adyen.checkout.example.data.api.model import androidx.annotation.Keep @Keep -data class AdditionalData( - val allow3DS2: String = "false", - val executeThreeD: String = "false" +data class ThreeDSRequestData( + val nativeThreeDS: String = "preferred" ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/mock/MockDataService.kt b/example-app/src/main/java/com/adyen/checkout/example/data/mock/MockDataService.kt new file mode 100644 index 0000000000..d37151d7d4 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/data/mock/MockDataService.kt @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 3/1/2024. + */ + +package com.adyen.checkout.example.data.mock + +import android.content.res.AssetManager + +class MockDataService( + private val assetManager: AssetManager +) { + fun readJsonFile(fileName: String): String { + return assetManager.open(fileName).bufferedReader().use { it.readText() } + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/mock/model/MockAddressLookupResponse.kt b/example-app/src/main/java/com/adyen/checkout/example/data/mock/model/MockAddressLookupResponse.kt new file mode 100644 index 0000000000..8003a3d761 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/data/mock/model/MockAddressLookupResponse.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 3/1/2024. + */ + +package com.adyen.checkout.example.data.mock.model + +import com.adyen.checkout.components.core.LookupAddress + +data class MockAddressLookupResponse( + val options: List +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/storage/CardAddressMode.kt b/example-app/src/main/java/com/adyen/checkout/example/data/storage/CardAddressMode.kt index 3575306e03..539cf1fb76 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/storage/CardAddressMode.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/storage/CardAddressMode.kt @@ -15,4 +15,5 @@ enum class CardAddressMode { NONE, POSTAL_CODE, FULL_ADDRESS, + LOOKUP, } diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt b/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt index 255bd15b86..865c5f9034 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt @@ -23,13 +23,13 @@ interface KeyValueStorage { fun getShopperReference(): String fun getAmount(): Amount fun getCountry(): String - fun getShopperLocale(): String - fun isThreeds2Enabled(): Boolean - fun isExecuteThreeD(): Boolean + fun getShopperLocale(): String? + fun getThreeDSMode(): ThreeDSMode fun getShopperEmail(): String fun getMerchantAccount(): String fun isSplitCardFundingSources(): Boolean fun getCardAddressMode(): CardAddressMode + fun isRemoveStoredPaymentMethodEnabled(): Boolean fun getInstantPaymentMethodType(): String fun getInstallmentOptionsMode(): CardInstallmentOptionsMode fun isInstallmentAmountShown(): Boolean @@ -48,7 +48,7 @@ internal class DefaultKeyValueStorage( return sharedPreferences.getString( appContext = appContext, stringRes = R.string.shopper_reference_key, - defaultValue = BuildConfig.SHOPPER_REFERENCE, + defaultStringRes = R.string.preferences_default_shopper_reference, ) } @@ -63,7 +63,7 @@ internal class DefaultKeyValueStorage( appContext = appContext, stringRes = R.string.amount_value_key, defaultStringRes = R.string.preferences_default_amount_value, - ).toLong() + ).toLong(), ) } @@ -75,27 +75,17 @@ internal class DefaultKeyValueStorage( ) } - override fun getShopperLocale(): String { - return sharedPreferences.getString( - appContext = appContext, - stringRes = R.string.shopper_locale_key, - defaultStringRes = R.string.preferences_default_shopper_locale, - ) - } - - override fun isThreeds2Enabled(): Boolean { - return sharedPreferences.getBoolean( - appContext = appContext, - stringRes = R.string.threeds2_key, - defaultStringRes = R.string.preferences_default_threeds2_enabled, - ) + override fun getShopperLocale(): String? { + return sharedPreferences.getString(appContext.getString(R.string.shopper_locale_key), null) } - override fun isExecuteThreeD(): Boolean { - return sharedPreferences.getBoolean( - appContext = appContext, - stringRes = R.string.execute3D_key, - defaultStringRes = R.string.preferences_default_execute_threed, + override fun getThreeDSMode(): ThreeDSMode { + return ThreeDSMode.valueOf( + sharedPreferences.getString( + appContext = appContext, + stringRes = R.string.threeds_mode_key, + defaultStringRes = R.string.preferences_default_threeds_mode, + ), ) } @@ -129,10 +119,16 @@ internal class DefaultKeyValueStorage( appContext = appContext, stringRes = R.string.card_address_form_mode_key, defaultStringRes = R.string.preferences_default_address_form_mode, - ) + ), ) } + override fun isRemoveStoredPaymentMethodEnabled() = sharedPreferences.getBoolean( + appContext = appContext, + stringRes = R.string.remove_stored_payment_method_key, + defaultStringRes = R.string.preferences_default_remove_stored_payment_method, + ) + override fun getInstantPaymentMethodType(): String { return sharedPreferences.getString( appContext = appContext, @@ -147,7 +143,7 @@ internal class DefaultKeyValueStorage( appContext = appContext, stringRes = R.string.card_installment_options_mode_key, defaultStringRes = R.string.preferences_default_installment_options_mode, - ) + ), ) } @@ -177,7 +173,7 @@ internal class DefaultKeyValueStorage( appContext = appContext, stringRes = R.string.analytics_level_key, defaultStringRes = R.string.preferences_default_analytics_level, - ) + ), ) } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/storage/ThreeDSMode.kt b/example-app/src/main/java/com/adyen/checkout/example/data/storage/ThreeDSMode.kt new file mode 100644 index 0000000000..05ff050fc9 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/data/storage/ThreeDSMode.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 29/2/2024. + */ + +package com.adyen.checkout.example.data.storage + +import androidx.annotation.Keep + +@Keep +enum class ThreeDSMode { + PREFER_NATIVE, + REDIRECT, + DISABLED, +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt index e4ad77f02f..b0ad82c959 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt @@ -29,29 +29,30 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { - private val BASE_URL = if (CheckoutApiService.isRealUrlAvailable()) { - BuildConfig.MERCHANT_SERVER_URL - } else { - "http://myserver.com/my/endpoint/" - } - @Singleton @Provides internal fun provideOkHttpClient(): OkHttpClient { val builder = OkHttpClient.Builder() + val authorizationHeader = (BuildConfig.AUTHORIZATION_HEADER_NAME to BuildConfig.AUTHORIZATION_HEADER_VALUE) + .takeIf { it.first.isNotBlank() } + if (BuildConfig.DEBUG) { - val interceptor = HttpLoggingInterceptor() - interceptor.level = HttpLoggingInterceptor.Level.BODY + val interceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + if (authorizationHeader != null) redactHeader(authorizationHeader.first) + } builder.addNetworkInterceptor(interceptor) } - builder.addInterceptor { chain -> - val request = chain.request().newBuilder() - .addHeader(BuildConfig.API_KEY_HEADER_NAME, BuildConfig.CHECKOUT_API_KEY) - .build() + if (authorizationHeader != null) { + builder.addInterceptor { chain -> + val request = chain.request().newBuilder() + .header(authorizationHeader.first, authorizationHeader.second) + .build() - chain.proceed(request) + chain.proceed(request) + } } return builder.build() @@ -63,7 +64,7 @@ object NetworkModule { Moshi.Builder() .add(JSONObjectAdapter()) .add(KotlinJsonAdapterFactory()) - .build() + .build(), ) @Singleton @@ -74,7 +75,7 @@ object NetworkModule { converterFactory: Converter.Factory, ): Retrofit = Retrofit.Builder() - .baseUrl(BASE_URL) + .baseUrl(BuildConfig.MERCHANT_SERVER_URL) .client(okHttpClient) .addConverterFactory(converterFactory) .build() diff --git a/example-app/src/main/java/com/adyen/checkout/example/di/StorageModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/StorageModule.kt index 5b2abb2e1c..d972998d95 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/StorageModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/StorageModule.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.example.di import android.app.Application import android.content.SharedPreferences +import android.content.res.AssetManager import androidx.preference.PreferenceManager import com.adyen.checkout.example.data.storage.DefaultKeyValueStorage import com.adyen.checkout.example.data.storage.KeyValueStorage @@ -29,4 +30,7 @@ object StorageModule { @Provides fun provideKeyValueStorage(appContext: Application, sharedPreferences: SharedPreferences): KeyValueStorage = DefaultKeyValueStorage(appContext, sharedPreferences) + + @Provides + fun provideAssetManager(appContext: Application): AssetManager = appContext.assets } diff --git a/example-app/src/main/java/com/adyen/checkout/example/extensions/LogExtensions.kt b/example-app/src/main/java/com/adyen/checkout/example/extensions/LogExtensions.kt index 73a225bdf4..1c300c1a00 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/extensions/LogExtensions.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/extensions/LogExtensions.kt @@ -8,9 +8,12 @@ package com.adyen.checkout.example.extensions -import com.adyen.checkout.core.internal.util.LogUtil - -@Suppress("RestrictedApi", "NOTHING_TO_INLINE") -internal inline fun getLogTag(): String { - return LogUtil.getTag() +internal fun Any.getLogTag(): String { + val fullClassName = this::class.java.name + val outerClassName = fullClassName.substringBefore('$').substringAfterLast('.') + return "EX." + if (outerClassName.isEmpty()) { + fullClassName + } else { + outerClassName.removeSuffix("Kt") + } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/repositories/AddressLookupRepository.kt b/example-app/src/main/java/com/adyen/checkout/example/repositories/AddressLookupRepository.kt new file mode 100644 index 0000000000..e6fd410b5f --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/repositories/AddressLookupRepository.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 3/1/2024. + */ + +package com.adyen.checkout.example.repositories + +import android.content.res.AssetManager +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.example.data.mock.MockDataService +import com.adyen.checkout.example.data.mock.model.MockAddressLookupResponse +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AddressLookupRepository @Inject constructor( + private val assetManager: AssetManager +) { + + fun getAddressLookupOptions(): List { + val mockDataService = MockDataService(assetManager) + val json = mockDataService.readJsonFile("lookup_options.json") + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val adapter: JsonAdapter = moshi.adapter(MockAddressLookupResponse::class.java) + return adapter.fromJson(json)?.options.orEmpty() + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/service/ExampleAdvancedDropInService.kt b/example-app/src/main/java/com/adyen/checkout/example/service/ExampleAdvancedDropInService.kt index 021e82839e..7d540508e0 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/service/ExampleAdvancedDropInService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/service/ExampleAdvancedDropInService.kt @@ -14,6 +14,7 @@ import com.adyen.checkout.card.CardComponentState import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.BalanceResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.PaymentComponentData @@ -22,6 +23,7 @@ import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.dropin.AddressLookupDropInServiceResult import com.adyen.checkout.dropin.BalanceDropInServiceResult import com.adyen.checkout.dropin.DropInService import com.adyen.checkout.dropin.DropInServiceResult @@ -32,10 +34,17 @@ import com.adyen.checkout.dropin.RecurringDropInServiceResult import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.extensions.getLogTag import com.adyen.checkout.example.extensions.toStringPretty +import com.adyen.checkout.example.repositories.AddressLookupRepository import com.adyen.checkout.example.repositories.PaymentsRepository import com.adyen.checkout.redirect.RedirectComponent import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.json.JSONObject import javax.inject.Inject @@ -48,6 +57,7 @@ import javax.inject.Inject * [onOrderRequest] and [onOrderCancel] and it handles the stored payment method removal flow by * implementing [onRemoveStoredPaymentMethod]. */ +@OptIn(FlowPreview::class) @Suppress("TooManyFunctions") @AndroidEntryPoint class ExampleAdvancedDropInService : DropInService() { @@ -58,6 +68,25 @@ class ExampleAdvancedDropInService : DropInService() { @Inject lateinit var keyValueStorage: KeyValueStorage + @Inject + lateinit var addressLookupRepository: AddressLookupRepository + + private val addressLookupQueryFlow = MutableStateFlow(null) + + init { + addressLookupQueryFlow + .debounce(ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION) + .filterNotNull() + .onEach { query -> + val options = if (query == "empty") { + emptyList() + } else { + addressLookupRepository.getAddressLookupOptions() + } + sendAddressLookupResult(AddressLookupDropInServiceResult.LookupResult(options)) + }.launchIn(this) + } + override fun onSubmit( state: PaymentComponentState<*>, ) { @@ -76,9 +105,8 @@ class ExampleAdvancedDropInService : DropInService() { countryCode = keyValueStorage.getCountry(), merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = RedirectComponent.getReturnUrl(applicationContext), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - shopperEmail = keyValueStorage.getShopperEmail() + threeDSMode = keyValueStorage.getThreeDSMode(), + shopperEmail = keyValueStorage.getShopperEmail(), ) Log.v(TAG, "paymentComponentJson - ${paymentComponentJson.toStringPretty()}") @@ -187,7 +215,7 @@ class ExampleAdvancedDropInService : DropInService() { val order = orderResponse?.let { Order( pspReference = it.pspReference, - orderData = it.orderData + orderData = it.orderData, ) } val paymentMethodRequest = getPaymentMethodRequest( @@ -222,7 +250,7 @@ class ExampleAdvancedDropInService : DropInService() { val request = createBalanceRequest( paymentMethodJson, amountJson, - keyValueStorage.getMerchantAccount() + keyValueStorage.getMerchantAccount(), ) val response = paymentsRepository.getBalance(request) @@ -230,7 +258,7 @@ class ExampleAdvancedDropInService : DropInService() { sendBalanceResult(result) } else { val result = BalanceDropInServiceResult.Error( - errorDialog = ErrorDialog(message = "amount or paymentMethod is null.") + errorDialog = ErrorDialog(message = "amount or paymentMethod is null."), ) sendBalanceResult(result) } @@ -248,14 +276,14 @@ class ExampleAdvancedDropInService : DropInService() { } catch (e: ModelSerializationException) { BalanceDropInServiceResult.Error( errorDialog = ErrorDialog(message = "Not enough balance"), - dismissDropIn = false + dismissDropIn = false, ) } } else -> BalanceDropInServiceResult.Error( errorDialog = ErrorDialog(message = resultCode), - dismissDropIn = false + dismissDropIn = false, ) } } else { @@ -270,7 +298,7 @@ class ExampleAdvancedDropInService : DropInService() { val paymentRequest = createOrderRequest( keyValueStorage.getAmount(), - keyValueStorage.getMerchantAccount() + keyValueStorage.getMerchantAccount(), ) val response = paymentsRepository.createOrder(paymentRequest) @@ -286,7 +314,7 @@ class ExampleAdvancedDropInService : DropInService() { "Success" -> OrderDropInServiceResult.OrderCreated(OrderResponse.SERIALIZER.deserialize(jsonResponse)) else -> OrderDropInServiceResult.Error( errorDialog = ErrorDialog(message = resultCode), - dismissDropIn = false + dismissDropIn = false, ) } } else { @@ -301,7 +329,7 @@ class ExampleAdvancedDropInService : DropInService() { val orderJson = Order.SERIALIZER.serialize(order) val request = createCancelOrderRequest( orderJson, - keyValueStorage.getMerchantAccount() + keyValueStorage.getMerchantAccount(), ) val response = paymentsRepository.cancelOrder(request) @@ -324,7 +352,7 @@ class ExampleAdvancedDropInService : DropInService() { else -> DropInServiceResult.Error( errorDialog = ErrorDialog(message = resultCode), - dismissDropIn = false + dismissDropIn = false, ) } } else { @@ -373,8 +401,20 @@ class ExampleAdvancedDropInService : DropInService() { Log.d(TAG, "On bin lookup: ${data.map { it.brand }}") } + override fun onAddressLookupQueryChanged(query: String) { + Log.d(TAG, "On address lookup query: $query") + addressLookupQueryFlow.tryEmit(query) + } + + override fun onAddressLookupCompletion(lookupAddress: LookupAddress): Boolean { + Log.d(TAG, "On address lookup query completion: $lookupAddress") + return false + } + companion object { private val TAG = getLogTag() private const val RESULT_REFUSED = "refused" + + private const val ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION = 300L } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/service/ExampleDropInService.kt b/example-app/src/main/java/com/adyen/checkout/example/service/ExampleDropInService.kt index fd0483f49c..6805bf99be 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/service/ExampleDropInService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/service/ExampleDropInService.kt @@ -60,8 +60,7 @@ class ExampleDropInService : DropInService() { countryCode = keyValueStorage.getCountry(), merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = RedirectComponent.getReturnUrl(applicationContext), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail() ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/service/ExampleSessionsDropInService.kt b/example-app/src/main/java/com/adyen/checkout/example/service/ExampleSessionsDropInService.kt index 0c0d023bce..e3fa9acd74 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/service/ExampleSessionsDropInService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/service/ExampleSessionsDropInService.kt @@ -57,9 +57,8 @@ class ExampleSessionsDropInService : SessionDropInService() { countryCode = keyValueStorage.getCountry(), merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = RedirectComponent.getReturnUrl(applicationContext), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - shopperEmail = keyValueStorage.getShopperEmail() + threeDSMode = keyValueStorage.getThreeDSMode(), + shopperEmail = keyValueStorage.getShopperEmail(), ) Log.v(TAG, "paymentComponentJson - ${paymentComponentJson.toStringPretty()}") @@ -82,7 +81,7 @@ class ExampleSessionsDropInService : SessionDropInService() { Log.d(TAG, "onDetailsCallRequested") val response = paymentsRepository.makeDetailsRequest( - ActionComponentData.SERIALIZER.serialize(actionComponentData) + ActionComponentData.SERIALIZER.serialize(actionComponentData), ) val result = handleResponse(response) diff --git a/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt b/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt index f7cdf3c256..c894d9a654 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/service/RequestUtils.kt @@ -12,7 +12,7 @@ package com.adyen.checkout.example.service import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.OrderRequest -import com.adyen.checkout.example.data.api.model.AdditionalData +import com.adyen.checkout.example.data.api.model.AuthenticationData import com.adyen.checkout.example.data.api.model.BalanceRequest import com.adyen.checkout.example.data.api.model.CancelOrderRequest import com.adyen.checkout.example.data.api.model.CreateOrderRequest @@ -24,8 +24,9 @@ import com.adyen.checkout.example.data.api.model.PaymentsRequestData import com.adyen.checkout.example.data.api.model.RecurringProcessingModel import com.adyen.checkout.example.data.api.model.SessionRequest import com.adyen.checkout.example.data.api.model.StorePaymentMethodMode -import com.adyen.checkout.example.data.api.model.ThreeDS2RequestDataRequest +import com.adyen.checkout.example.data.api.model.ThreeDSRequestData import com.adyen.checkout.example.data.storage.CardInstallmentOptionsMode +import com.adyen.checkout.example.data.storage.ThreeDSMode import com.adyen.checkout.sessions.core.SessionSetupInstallmentOptions import org.json.JSONObject @@ -35,7 +36,7 @@ fun getPaymentMethodRequest( shopperReference: String, amount: Amount?, countryCode: String, - shopperLocale: String, + shopperLocale: String?, splitCardFundingSources: Boolean, order: OrderRequest? = null, ): PaymentMethodsRequest { @@ -57,13 +58,13 @@ fun getSessionRequest( shopperReference: String, amount: Amount?, countryCode: String, - shopperLocale: String, + shopperLocale: String?, splitCardFundingSources: Boolean, redirectUrl: String, - isThreeds2Enabled: Boolean, - isExecuteThreeD: Boolean, + threeDSMode: ThreeDSMode, installmentOptions: Map?, showInstallmentAmount: Boolean = false, + showRemovePaymentMethodButton: Boolean = false, threeDSAuthenticationOnly: Boolean = false, shopperEmail: String? = null, allowedPaymentMethods: List? = null, @@ -81,18 +82,16 @@ fun getSessionRequest( shopperIP = SHOPPER_IP, reference = getReference(), channel = CHANNEL, - additionalData = getAdditionalData(isThreeds2Enabled = isThreeds2Enabled, isExecuteThreeD = isExecuteThreeD), + authenticationData = getAuthenticationData(threeDSMode), lineItems = LINE_ITEMS, threeDSAuthenticationOnly = threeDSAuthenticationOnly, - // TODO check if this should be kept or removed - // previous code: if (force3DS2Challenge) ThreeDS2RequestDataRequest() else null - threeDS2RequestData = null, shopperEmail = shopperEmail, allowedPaymentMethods = allowedPaymentMethods, storePaymentMethodMode = storePaymentMethodMode, recurringProcessingModel = recurringProcessingModel, installmentOptions = installmentOptions, - showInstallmentAmount = showInstallmentAmount + showInstallmentAmount = showInstallmentAmount, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, ) } @@ -104,10 +103,8 @@ fun createPaymentRequest( countryCode: String, merchantAccount: String, redirectUrl: String, - isThreeds2Enabled: Boolean, - isExecuteThreeD: Boolean, + threeDSMode: ThreeDSMode, shopperEmail: String?, - force3DS2Challenge: Boolean = true, threeDSAuthenticationOnly: Boolean = false, recurringProcessingModel: String? = RecurringProcessingModel.SUBSCRIPTION.recurringModel, ): PaymentsRequest { @@ -120,11 +117,10 @@ fun createPaymentRequest( shopperIP = SHOPPER_IP, reference = getReference(), channel = CHANNEL, - additionalData = getAdditionalData(isThreeds2Enabled = isThreeds2Enabled, isExecuteThreeD = isExecuteThreeD), + authenticationData = getAuthenticationData(threeDSMode), lineItems = LINE_ITEMS, shopperEmail = shopperEmail, threeDSAuthenticationOnly = threeDSAuthenticationOnly, - threeDS2RequestData = if (force3DS2Challenge) ThreeDS2RequestDataRequest() else null, recurringProcessingModel = recurringProcessingModel, ) @@ -138,24 +134,24 @@ fun createBalanceRequest( ) = BalanceRequest( paymentMethod = paymentComponentData, amount = amount, - merchantAccount = merchantAccount + merchantAccount = merchantAccount, ) fun createOrderRequest( amount: Amount, - merchantAccount: String + merchantAccount: String, ) = CreateOrderRequest( amount = amount, merchantAccount = merchantAccount, - reference = getReference() + reference = getReference(), ) fun createCancelOrderRequest( orderJson: JSONObject, - merchantAccount: String + merchantAccount: String, ) = CancelOrderRequest( order = orderJson, - merchantAccount = merchantAccount + merchantAccount = merchantAccount, ) private const val SHOPPER_IP = "142.12.31.22" @@ -163,27 +159,43 @@ private const val CHANNEL = "android" private val LINE_ITEMS = listOf(Item()) private const val DEFAULT_INSTALLMENT_OPTION = "card" private const val CARD_BASED_INSTALLMENT_OPTION = "visa" +private const val ATTEMPT_AUTHENTICATION_TRUE_VALUE = "always" +private const val ATTEMPT_AUTHENTICATION_FALSE_VALUE = "never" private fun getReference() = "android-test-components_${System.currentTimeMillis()}" -private fun getAdditionalData(isThreeds2Enabled: Boolean, isExecuteThreeD: Boolean) = AdditionalData( - allow3DS2 = isThreeds2Enabled.toString(), - executeThreeD = isExecuteThreeD.toString() -) +private fun getAuthenticationData(threeDSMode: ThreeDSMode): AuthenticationData { + return when (threeDSMode) { + ThreeDSMode.PREFER_NATIVE -> AuthenticationData( + attemptAuthentication = ATTEMPT_AUTHENTICATION_TRUE_VALUE, + threeDSRequestData = ThreeDSRequestData(), + ) + + ThreeDSMode.REDIRECT -> AuthenticationData( + attemptAuthentication = ATTEMPT_AUTHENTICATION_TRUE_VALUE, + threeDSRequestData = null, + ) + + ThreeDSMode.DISABLED -> AuthenticationData( + attemptAuthentication = ATTEMPT_AUTHENTICATION_FALSE_VALUE, + threeDSRequestData = null, + ) + } +} fun getSettingsInstallmentOptionsMode(settingsInstallmentOptionMode: CardInstallmentOptionsMode) = when (settingsInstallmentOptionMode) { CardInstallmentOptionsMode.NONE -> null CardInstallmentOptionsMode.DEFAULT -> mapOf( - DEFAULT_INSTALLMENT_OPTION to getSessionInstallmentOption() + DEFAULT_INSTALLMENT_OPTION to getSessionInstallmentOption(), ) CardInstallmentOptionsMode.DEFAULT_WITH_REVOLVING -> mapOf( - DEFAULT_INSTALLMENT_OPTION to getSessionInstallmentOption(plans = listOf(InstallmentPlan.REVOLVING.plan)) + DEFAULT_INSTALLMENT_OPTION to getSessionInstallmentOption(plans = listOf(InstallmentPlan.REVOLVING.plan)), ) CardInstallmentOptionsMode.CARD_BASED_VISA -> mapOf( - CARD_BASED_INSTALLMENT_OPTION to getSessionInstallmentOption() + CARD_BASED_INSTALLMENT_OPTION to getSessionInstallmentOption(), ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt index ca01899b46..5189aeead1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt @@ -89,7 +89,7 @@ class BacsFragment : BottomSheetDialogFragment() { val bacsComponent = BacsDirectDebitComponent.PROVIDER.get( fragment = this, paymentMethod = bacsComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getBacsConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, callback = bacsComponentData.callback ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsViewModel.kt index 29bdd3737f..a74892d543 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsViewModel.kt @@ -153,8 +153,7 @@ internal class BacsViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(BacsFragment.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikActivity.kt index 3200e444e8..03f5c99f38 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikActivity.kt @@ -97,7 +97,7 @@ class BlikActivity : AppCompatActivity() { val blikComponent = BlikComponent.PROVIDER.get( activity = this, paymentMethod = componentData.paymentMethod, - configuration = checkoutConfigurationProvider.getBlikConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, callback = componentData.callback ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikViewModel.kt index 6e701b6765..3c2d541217 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/blik/BlikViewModel.kt @@ -116,8 +116,7 @@ class BlikViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(BlikActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardActivity.kt index 051aaedacc..5b5216178f 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardActivity.kt @@ -3,6 +3,7 @@ package com.adyen.checkout.example.ui.card import android.content.Intent import android.os.Bundle import android.util.Log +import android.view.MenuItem import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -11,6 +12,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.example.databinding.ActivityCardBinding import com.adyen.checkout.example.extensions.getLogTag @@ -21,7 +25,8 @@ import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint -class CardActivity : AppCompatActivity() { +@Suppress("TooManyFunctions") +class CardActivity : AppCompatActivity(), AddressLookupCallback { @Inject internal lateinit var checkoutConfigurationProvider: CheckoutConfigurationProvider @@ -55,6 +60,14 @@ class CardActivity : AppCompatActivity() { } } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == android.R.id.home && cardComponent?.handleBackPress() == true) { + true + } else { + super.onOptionsItemSelected(item) + } + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) @@ -92,7 +105,7 @@ class CardActivity : AppCompatActivity() { val cardComponent = CardComponent.PROVIDER.get( activity = this, paymentMethod = cardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getCardConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, callback = cardComponentData.callback, ) @@ -108,6 +121,8 @@ class CardActivity : AppCompatActivity() { Log.d(TAG, "On bin lookup: ${data.map { it.brand }}") } + cardComponent.setAddressLookupCallback(this) + this.cardComponent = cardComponent binding.cardView.attach(cardComponent, this) @@ -117,6 +132,9 @@ class CardActivity : AppCompatActivity() { when (event) { is CardEvent.PaymentResult -> onPaymentResult(event.result) is CardEvent.AdditionalAction -> onAction(event.action) + is CardEvent.AddressLookup -> onAddressLookup(event.options) + is CardEvent.AddressLookupCompleted -> onAddressLookupCompleted(event.lookupAddress) + is CardEvent.AddressLookupError -> onAddressLookupError(event.message) } } @@ -129,6 +147,34 @@ class CardActivity : AppCompatActivity() { cardComponent?.handleAction(action, this) } + private fun onAddressLookup(options: List) { + cardComponent?.updateAddressLookupOptions(options) + } + + private fun onAddressLookupCompleted(lookupAddress: LookupAddress) { + cardComponent?.setAddressLookupResult(AddressLookupResult.Completed(lookupAddress)) + } + + private fun onAddressLookupError(message: String) { + cardComponent?.setAddressLookupResult(AddressLookupResult.Error(message)) + } + + override fun onQueryChanged(query: String) { + Log.d(TAG, "On address lookup query changed: $query") + cardViewModel.onAddressLookupQueryChanged(query) + } + + override fun onLookupCompletion(lookupAddress: LookupAddress): Boolean { + Log.d(TAG, "on lookup completed $lookupAddress") + cardViewModel.onAddressLookupCompleted(lookupAddress) + return true + } + + override fun onBackPressed() { + if (cardComponent?.handleBackPress() == true) return + super.onBackPressed() + } + override fun onDestroy() { super.onDestroy() cardComponent = null diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardEvent.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardEvent.kt index 9681d38b4b..7558bb9b27 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardEvent.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardEvent.kt @@ -8,6 +8,7 @@ package com.adyen.checkout.example.ui.card +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.action.Action internal sealed class CardEvent { @@ -15,4 +16,10 @@ internal sealed class CardEvent { data class PaymentResult(val result: String) : CardEvent() data class AdditionalAction(val action: Action) : CardEvent() + + data class AddressLookup(val options: List) : CardEvent() + + data class AddressLookupCompleted(val lookupAddress: LookupAddress) : CardEvent() + + data class AddressLookupError(val message: String) : CardEvent() } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardViewModel.kt index 8957a252ce..ff3c8a2530 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/CardViewModel.kt @@ -8,28 +8,38 @@ import com.adyen.checkout.card.CardComponentState import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.example.data.storage.KeyValueStorage +import com.adyen.checkout.example.repositories.AddressLookupRepository import com.adyen.checkout.example.repositories.PaymentsRepository import com.adyen.checkout.example.service.createPaymentRequest import com.adyen.checkout.example.service.getPaymentMethodRequest import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.json.JSONObject import javax.inject.Inject +@OptIn(FlowPreview::class) +@Suppress("TooManyFunctions") @HiltViewModel internal class CardViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val paymentsRepository: PaymentsRepository, private val keyValueStorage: KeyValueStorage, + private val addressLookupRepository: AddressLookupRepository ) : ViewModel(), ComponentCallback { private val _cardComponentDataFlow = MutableStateFlow(null) @@ -38,11 +48,25 @@ internal class CardViewModel @Inject constructor( private val _cardViewState = MutableStateFlow(CardViewState.Loading) val cardViewState: Flow = _cardViewState + private val addressLookupQueryFlow = MutableStateFlow(null) + private val _events = MutableSharedFlow() val events: Flow = _events init { viewModelScope.launch { fetchPaymentMethods() } + addressLookupQueryFlow + .filterNotNull() + .debounce(ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION) + .onEach { query -> + val options = if (query == "empty") { + emptyList() + } else { + addressLookupRepository.getAddressLookupOptions() + } + _events.emit(CardEvent.AddressLookup(options)) + } + .launchIn(viewModelScope) } private suspend fun fetchPaymentMethods() = withContext(Dispatchers.IO) { @@ -54,7 +78,7 @@ internal class CardViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - ) + ), ) val cardPaymentMethod = paymentMethodResponse @@ -68,7 +92,7 @@ internal class CardViewModel @Inject constructor( CardComponentData( paymentMethod = cardPaymentMethod, callback = this@CardViewModel, - ) + ), ) _cardViewState.emit(CardViewState.ShowComponent) } @@ -86,6 +110,27 @@ internal class CardViewModel @Inject constructor( onComponentError(componentError) } + fun onAddressLookupQueryChanged(query: String) { + viewModelScope.launch { + addressLookupQueryFlow.emit(query) + } + } + + fun onAddressLookupCompleted(lookupAddress: LookupAddress) { + viewModelScope.launch { + delay(ADDRESS_LOOKUP_COMPLETION_DELAY) + if (lookupAddress.id == ADDRESS_LOOKUP_ERROR_ITEM_ID) { + _events.emit(CardEvent.AddressLookupError("Something went wrong.")) + } else { + _events.emit( + CardEvent.AddressLookupCompleted( + addressLookupRepository.getAddressLookupOptions().first { it.id == lookupAddress.id }, + ), + ) + } + } + } + // no ops override fun onStateChanged(state: CardComponentState) = Unit @@ -103,8 +148,7 @@ internal class CardViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(CardActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) @@ -119,6 +163,7 @@ internal class CardViewModel @Inject constructor( val action = Action.SERIALIZER.deserialize(json.getJSONObject("action")) handleAction(action) } + else -> _events.emit(CardEvent.PaymentResult("Finished: ${json.optString("resultCode")}")) } } ?: _events.emit(CardEvent.PaymentResult("Failed")) @@ -138,4 +183,10 @@ internal class CardViewModel @Inject constructor( private fun onComponentError(error: ComponentError) { viewModelScope.launch { _events.emit(CardEvent.PaymentResult("Failed: ${error.errorMessage}")) } } + + companion object { + private const val ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION = 300L + private const val ADDRESS_LOOKUP_COMPLETION_DELAY = 400L + private const val ADDRESS_LOOKUP_ERROR_ITEM_ID = "error" + } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverActivity.kt index f390faf020..87b854ba4c 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverActivity.kt @@ -19,6 +19,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.adyen.checkout.card.CardComponent +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.LookupAddress import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.example.databinding.ActivityCardBinding import com.adyen.checkout.example.extensions.getLogTag @@ -29,7 +31,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint -class SessionsCardTakenOverActivity : AppCompatActivity() { +class SessionsCardTakenOverActivity : AppCompatActivity(), AddressLookupCallback { @Inject internal lateinit var checkoutConfigurationProvider: CheckoutConfigurationProvider @@ -101,14 +103,16 @@ class SessionsCardTakenOverActivity : AppCompatActivity() { activity = this, checkoutSession = sessionsCardComponentData.checkoutSession, paymentMethod = sessionsCardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getCardConfiguration(), - componentCallback = sessionsCardComponentData.callback + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, + componentCallback = sessionsCardComponentData.callback, ) cardComponent.setOnRedirectListener { Log.d(TAG, "On redirect") } + cardComponent.setAddressLookupCallback(this) + this.cardComponent = cardComponent binding.cardView.attach(cardComponent, this) @@ -118,6 +122,9 @@ class SessionsCardTakenOverActivity : AppCompatActivity() { when (event) { is CardEvent.PaymentResult -> onPaymentResult(event.result) is CardEvent.AdditionalAction -> onAction(event.action) + is CardEvent.AddressLookup -> onAddressLookup(event.options) + is CardEvent.AddressLookupCompleted -> {} + is CardEvent.AddressLookupError -> {} } } @@ -130,6 +137,15 @@ class SessionsCardTakenOverActivity : AppCompatActivity() { finish() } + private fun onAddressLookup(options: List) { + cardComponent?.updateAddressLookupOptions(options) + } + + override fun onQueryChanged(query: String) { + Log.d(TAG, "On address lookup query changed: $query") + cardViewModel.onAddressLookupQueryChanged(query) + } + override fun onDestroy() { super.onDestroy() cardComponent = null diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt index 04ecdcb2e9..9239b30560 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardTakenOverViewModel.kt @@ -14,15 +14,16 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.adyen.checkout.card.CardBrand import com.adyen.checkout.card.CardComponentState -import com.adyen.checkout.card.CardConfiguration import com.adyen.checkout.card.CardType import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.extensions.getLogTag +import com.adyen.checkout.example.repositories.AddressLookupRepository import com.adyen.checkout.example.repositories.PaymentsRepository import com.adyen.checkout.example.service.createPaymentRequest import com.adyen.checkout.example.service.getSessionRequest @@ -36,20 +37,26 @@ import com.adyen.checkout.sessions.core.SessionModel import com.adyen.checkout.sessions.core.SessionPaymentResult import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.json.JSONObject import javax.inject.Inject +@OptIn(FlowPreview::class) @Suppress("TooManyFunctions") @HiltViewModel internal class SessionsCardTakenOverViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, private val paymentsRepository: PaymentsRepository, private val keyValueStorage: KeyValueStorage, + private val addressLookupRepository: AddressLookupRepository, checkoutConfigurationProvider: CheckoutConfigurationProvider, ) : ViewModel(), SessionComponentCallback { @@ -62,7 +69,9 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( private val _events = MutableSharedFlow() val events: Flow = _events - private val cardConfiguration = checkoutConfigurationProvider.getCardConfiguration() + private val addressLookupQueryFlow = MutableStateFlow(null) + + private val checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig private var isFlowTakenOver: Boolean get() = savedStateHandle[IS_SESSIONS_FLOW_TAKEN_OVER_KEY] ?: false @@ -72,6 +81,18 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( init { viewModelScope.launch { launchComponent() } + addressLookupQueryFlow + .filterNotNull() + .debounce(ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION) + .onEach { query -> + val options = if (query == "empty") { + emptyList() + } else { + addressLookupRepository.getAddressLookupOptions() + } + _events.emit(CardEvent.AddressLookup(options)) + } + .launchIn(viewModelScope) } private suspend fun launchComponent() { @@ -93,8 +114,8 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( SessionsCardComponentData( checkoutSession = checkoutSession, paymentMethod = paymentMethod, - callback = this - ) + callback = this, + ), ) _cardViewState.emit(CardViewState.ShowComponent) } @@ -108,25 +129,25 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + threeDSMode = keyValueStorage.getThreeDSMode(), redirectUrl = savedStateHandle.get(SessionsCardTakenOverActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), - showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() - ) + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + showRemovePaymentMethodButton = keyValueStorage.isRemoveStoredPaymentMethodEnabled(), + ), ) ?: return null - return getCheckoutSession(sessionModel, cardConfiguration) + return getCheckoutSession(sessionModel, checkoutConfiguration) } private suspend fun getCheckoutSession( sessionModel: SessionModel, - cardConfiguration: CardConfiguration, + checkoutConfiguration: CheckoutConfiguration, ): CheckoutSession? { - return when (val result = CheckoutSessionProvider.createSession(sessionModel, cardConfiguration)) { + return when (val result = CheckoutSessionProvider.createSession(sessionModel, checkoutConfiguration)) { is CheckoutSessionResult.Success -> result.checkoutSession is CheckoutSessionResult.Error -> null } @@ -176,8 +197,7 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(SessionsCardTakenOverActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) @@ -192,6 +212,7 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( val action = Action.SERIALIZER.deserialize(json.getJSONObject("action")) handleAction(action) } + else -> _events.emit(CardEvent.PaymentResult("Finished: ${json.optString("resultCode")}")) } } ?: _events.emit(CardEvent.PaymentResult("Failed")) @@ -219,8 +240,16 @@ internal class SessionsCardTakenOverViewModel @Inject constructor( _cardViewState.tryEmit(state) } + fun onAddressLookupQueryChanged(query: String) { + viewModelScope.launch { + addressLookupQueryFlow.emit(query) + } + } + companion object { private val TAG = getLogTag() private const val IS_SESSIONS_FLOW_TAKEN_OVER_KEY = "IS_SESSIONS_FLOW_TAKEN_OVER_KEY" + + private const val ADDRESS_LOOKUP_QUERY_DEBOUNCE_DURATION = 300L } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt index 26981c1eac..b17fffa24b 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardUiState.kt @@ -9,13 +9,13 @@ package com.adyen.checkout.example.ui.card import androidx.compose.runtime.Immutable -import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.example.ui.compose.ResultState @Immutable internal data class SessionsCardUiState( - val cardConfiguration: CardConfiguration, + val checkoutConfiguration: CheckoutConfiguration, val isLoading: Boolean = false, val oneTimeMessage: String? = null, val componentData: SessionsCardComponentData? = null, diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt index aea2968943..df62587f49 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/SessionsCardViewModel.kt @@ -13,7 +13,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.adyen.checkout.card.CardComponentState -import com.adyen.checkout.card.CardConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.action.Action @@ -49,9 +49,9 @@ internal class SessionsCardViewModel @Inject constructor( checkoutConfigurationProvider: CheckoutConfigurationProvider, ) : ViewModel(), SessionComponentCallback { - private val cardConfiguration = checkoutConfigurationProvider.getCardConfiguration() + private val checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig - private val _uiState = MutableStateFlow(SessionsCardUiState(cardConfiguration)) + private val _uiState = MutableStateFlow(SessionsCardUiState(checkoutConfiguration)) val uiState: StateFlow = _uiState.asStateFlow() init { @@ -91,25 +91,25 @@ internal class SessionsCardViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + threeDSMode = keyValueStorage.getThreeDSMode(), redirectUrl = savedStateHandle.get(SessionsCardActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + showRemovePaymentMethodButton = keyValueStorage.isRemoveStoredPaymentMethodEnabled(), ), ) ?: return null - return getCheckoutSession(sessionModel, cardConfiguration) + return getCheckoutSession(sessionModel, checkoutConfiguration) } private suspend fun getCheckoutSession( sessionModel: SessionModel, - cardConfiguration: CardConfiguration, + checkoutConfiguration: CheckoutConfiguration, ): CheckoutSession? { - return when (val result = CheckoutSessionProvider.createSession(sessionModel, cardConfiguration)) { + return when (val result = CheckoutSessionProvider.createSession(sessionModel, checkoutConfiguration)) { is CheckoutSessionResult.Success -> result.checkoutSession is CheckoutSessionResult.Error -> null } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt index 1c433aea44..ce106ba8fc 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardScreen.kt @@ -39,9 +39,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.hilt.navigation.compose.hiltViewModel import com.adyen.checkout.card.CardComponent -import com.adyen.checkout.card.CardConfiguration import com.adyen.checkout.components.compose.AdyenComponent import com.adyen.checkout.components.compose.get +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.example.ui.card.SessionsCardComponentData import com.adyen.checkout.example.ui.card.SessionsCardUiState @@ -88,7 +88,7 @@ private fun SessionsCardContent( modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { - val (cardConfiguration, isLoading, oneTimeMessage, componentData, action, finalResult) = uiState + val (checkoutConfiguration, isLoading, oneTimeMessage, componentData, action, finalResult) = uiState if (isLoading) { CircularProgressIndicator() @@ -106,7 +106,7 @@ private fun SessionsCardContent( ResultContent(finalResult) } else if (componentData != null) { CardComponent( - configuration = cardConfiguration, + checkoutConfiguration = checkoutConfiguration, componentData = componentData, action = action, onActionConsumed = onActionConsumed, @@ -118,7 +118,7 @@ private fun SessionsCardContent( @Composable private fun CardComponent( - configuration: CardConfiguration, + checkoutConfiguration: CheckoutConfiguration, componentData: SessionsCardComponentData, action: Action?, onActionConsumed: () -> Unit, @@ -127,7 +127,7 @@ private fun CardComponent( val component = CardComponent.PROVIDER.get( componentData.checkoutSession, componentData.paymentMethod, - configuration, + checkoutConfiguration, componentData.callback, componentData.hashCode().toString(), ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt index db8117843b..4206855e70 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt @@ -1,30 +1,27 @@ package com.adyen.checkout.example.ui.configuration import android.content.Context -import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration -import com.adyen.checkout.bacs.BacsDirectDebitConfiguration -import com.adyen.checkout.bcmc.BcmcConfiguration -import com.adyen.checkout.blik.BlikConfiguration +import com.adyen.checkout.adyen3ds2.adyen3DS2 +import com.adyen.checkout.bcmc.bcmc import com.adyen.checkout.card.AddressConfiguration import com.adyen.checkout.card.CardBrand -import com.adyen.checkout.card.CardConfiguration import com.adyen.checkout.card.CardType import com.adyen.checkout.card.InstallmentConfiguration import com.adyen.checkout.card.InstallmentOptions +import com.adyen.checkout.card.card import com.adyen.checkout.cashapppay.CashAppPayComponent -import com.adyen.checkout.cashapppay.CashAppPayConfiguration +import com.adyen.checkout.cashapppay.cashAppPay import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.core.Environment -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.dropin.dropIn import com.adyen.checkout.example.BuildConfig import com.adyen.checkout.example.data.storage.CardAddressMode import com.adyen.checkout.example.data.storage.CardInstallmentOptionsMode import com.adyen.checkout.example.data.storage.KeyValueStorage -import com.adyen.checkout.giftcard.GiftCardConfiguration -import com.adyen.checkout.googlepay.GooglePayConfiguration -import com.adyen.checkout.instant.InstantPaymentConfiguration -import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.giftcard.giftCard +import com.adyen.checkout.googlepay.googlePay import dagger.hilt.android.qualifiers.ApplicationContext import java.util.Locale import javax.inject.Inject @@ -37,10 +34,10 @@ internal class CheckoutConfigurationProvider @Inject constructor( @ApplicationContext private val context: Context, ) { - private val shopperLocale: Locale + private val shopperLocale: Locale? get() { val shopperLocaleString = keyValueStorage.getShopperLocale() - return Locale.forLanguageTag(shopperLocaleString) + return shopperLocaleString?.let { Locale.forLanguageTag(it) } } private val amount: Amount get() = keyValueStorage.getAmount() @@ -49,65 +46,53 @@ internal class CheckoutConfigurationProvider @Inject constructor( private val environment = Environment.TEST - fun getDropInConfiguration(): DropInConfiguration = DropInConfiguration.Builder( - shopperLocale, - environment, - clientKey, - ) - .addCardConfiguration(getCardConfiguration()) - .addCashAppPayConfiguration(getCashAppPayConfiguration()) - .addBlikConfiguration(getBlikConfiguration()) - .addBacsDirectDebitConfiguration(getBacsConfiguration()) - .addBcmcConfiguration(getBcmcConfiguration()) - .addGooglePayConfiguration(getGooglePayConfiguration()) - .add3ds2ActionConfiguration(get3DS2Configuration()) - .addRedirectActionConfiguration(getRedirectConfiguration()) - .addGiftCardConfiguration(getGiftCardConfiguration()) - .setEnableRemovingStoredPaymentMethods(true) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - fun getCardConfiguration(): CardConfiguration = - CardConfiguration.Builder(shopperLocale, environment, clientKey) - .setShopperReference(keyValueStorage.getShopperReference()) - .setAddressConfiguration(getAddressConfiguration()) - .setInstallmentConfigurations(getInstallmentConfiguration()) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - private fun getCashAppPayConfiguration(): CashAppPayConfiguration = - CashAppPayConfiguration.Builder(shopperLocale, environment, clientKey) - .setReturnUrl(CashAppPayComponent.getReturnUrl(context)) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - fun getBlikConfiguration(): BlikConfiguration = - BlikConfiguration.Builder(shopperLocale, environment, clientKey) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - fun getBacsConfiguration(): BacsDirectDebitConfiguration = - BacsDirectDebitConfiguration.Builder(shopperLocale, environment, clientKey) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - fun getInstantConfiguration(): InstantPaymentConfiguration = - InstantPaymentConfiguration.Builder(shopperLocale, environment, clientKey) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - fun getGiftCardConfiguration(): GiftCardConfiguration = - GiftCardConfiguration.Builder(shopperLocale, environment, clientKey) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .setPinRequired(true) - .build() + val checkoutConfig: CheckoutConfiguration + get() = CheckoutConfiguration( + environment = environment, + clientKey = clientKey, + shopperLocale = shopperLocale, + amount = amount, + analyticsConfiguration = getAnalyticsConfiguration(), + ) { + // Drop-in + dropIn { + setEnableRemovingStoredPaymentMethods(keyValueStorage.isRemoveStoredPaymentMethodEnabled()) + } + + // Payment methods + bcmc { + setShopperReference(keyValueStorage.getShopperReference()) + setShowStorePaymentField(true) + } + + card { + setShopperReference(keyValueStorage.getShopperReference()) + setAddressConfiguration(getAddressConfiguration()) + setInstallmentConfigurations(getInstallmentConfiguration()) + } + + cashAppPay { + setReturnUrl(CashAppPayComponent.getReturnUrl(context)) + } + + giftCard { + setPinRequired(true) + } + + googlePay { + setCountryCode(keyValueStorage.getCountry()) + } + + // Actions + adyen3DS2 { + setThreeDSRequestorAppURL("https://www.adyen.com") + } + } + + private fun getAnalyticsConfiguration(): AnalyticsConfiguration { + val analyticsLevel = keyValueStorage.getAnalyticsLevel() + return AnalyticsConfiguration(level = analyticsLevel) + } private fun getAddressConfiguration(): AddressConfiguration = when (keyValueStorage.getCardAddressMode()) { CardAddressMode.NONE -> AddressConfiguration.None @@ -116,41 +101,11 @@ internal class CheckoutConfigurationProvider @Inject constructor( defaultCountryCode = null, supportedCountryCodes = listOf("NL", "GB", "US", "CA", "BR"), addressFieldPolicy = AddressConfiguration.CardAddressFieldPolicy.OptionalForCardTypes( - brands = listOf("jcb") - ) + brands = listOf("jcb"), + ), ) - } - - private fun getBcmcConfiguration(): BcmcConfiguration = - BcmcConfiguration.Builder(shopperLocale, environment, clientKey) - .setShopperReference(keyValueStorage.getShopperReference()) - .setShowStorePaymentField(true) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - fun getGooglePayConfiguration(): GooglePayConfiguration = - GooglePayConfiguration.Builder(shopperLocale, environment, clientKey) - .setCountryCode(keyValueStorage.getCountry()) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - private fun get3DS2Configuration(): Adyen3DS2Configuration = - Adyen3DS2Configuration.Builder(shopperLocale, environment, clientKey) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - - private fun getRedirectConfiguration(): RedirectConfiguration = - RedirectConfiguration.Builder(shopperLocale, environment, clientKey) - .setAmount(amount) - .setAnalyticsConfiguration(getAnalyticsConfiguration()) - .build() - private fun getAnalyticsConfiguration(): AnalyticsConfiguration { - val analyticsLevel = keyValueStorage.getAnalyticsLevel() - return AnalyticsConfiguration(level = analyticsLevel) + CardAddressMode.LOOKUP -> AddressConfiguration.Lookup() } private fun getInstallmentConfiguration(): InstallmentConfiguration = @@ -167,9 +122,9 @@ internal class CheckoutConfigurationProvider @Inject constructor( ) = InstallmentConfiguration( defaultOptions = InstallmentOptions.DefaultInstallmentOptions( maxInstallments = maxInstallments, - includeRevolving = includeRevolving + includeRevolving = includeRevolving, ), - showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), ) private fun getCardBasedInstallmentOptions( @@ -181,9 +136,9 @@ internal class CheckoutConfigurationProvider @Inject constructor( InstallmentOptions.CardBasedInstallmentOptions( maxInstallments = maxInstallments, includeRevolving = includeRevolving, - cardBrand = cardBrand - ) + cardBrand = cardBrand, + ), ), - showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt index 1605e39614..284e3b50b1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt @@ -12,8 +12,10 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.preference.DropDownPreference +import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat import com.adyen.checkout.example.R +import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.databinding.ActivitySettingsBinding import com.adyen.checkout.example.ui.theme.NightTheme import com.adyen.checkout.example.ui.theme.NightThemeRepository @@ -39,6 +41,9 @@ class ConfigurationActivity : AppCompatActivity() { @AndroidEntryPoint class ConfigurationFragment : PreferenceFragmentCompat() { + @Inject + lateinit var keyValueStorage: KeyValueStorage + @Inject internal lateinit var nightThemeRepository: NightThemeRepository @@ -49,12 +54,19 @@ class ConfigurationActivity : AppCompatActivity() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - preferenceManager.preferenceScreen - .findPreference(requireContext().getString(R.string.night_theme_title)) + preferenceManager + .findPreference(requireContext().getString(R.string.night_theme_key)) ?.setOnPreferenceChangeListener { _, newValue -> nightThemeRepository.theme = NightTheme.findByPreferenceValue(newValue as String?) true } + + /* This workaround is needed to display the default value of Merchant Account. We cannot set this value in + `preferences.xml` because it's only available in the code and there is no "clean" way to set the default + value programmatically. */ + preferenceManager + .findPreference(requireContext().getString(R.string.merchant_account_key)) + ?.text = keyValueStorage.getMerchantAccount() } } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardActivity.kt index 04a65bc90e..3eed267cbe 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardActivity.kt @@ -99,7 +99,7 @@ class GiftCardActivity : AppCompatActivity() { val giftCardComponent = GiftCardComponent.PROVIDER.get( activity = this, paymentMethod = giftCardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getGiftCardConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, callback = giftCardComponentData.callback, ) @@ -118,7 +118,7 @@ class GiftCardActivity : AppCompatActivity() { val giftCardComponent = GiftCardComponent.PROVIDER.get( activity = this, paymentMethod = giftCardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getGiftCardConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, callback = giftCardComponentData.callback, order = orderRequest, key = KEY_SECONDARY_GIFT_CARD_COMPONENT diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardViewModel.kt index e12575f298..a219c383e5 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/GiftCardViewModel.kt @@ -221,8 +221,7 @@ internal class GiftCardViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(GiftCardActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardActivity.kt index 46ea201cac..80d12d38aa 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardActivity.kt @@ -99,7 +99,7 @@ class SessionsGiftCardActivity : AppCompatActivity() { activity = this, checkoutSession = giftCardComponentData.checkoutSession, paymentMethod = giftCardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getGiftCardConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, componentCallback = giftCardComponentData.callback, ) @@ -121,7 +121,7 @@ class SessionsGiftCardActivity : AppCompatActivity() { activity = this, checkoutSession = giftCardComponentData.checkoutSession, paymentMethod = giftCardComponentData.paymentMethod, - configuration = checkoutConfigurationProvider.getGiftCardConfiguration(), + checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig, componentCallback = giftCardComponentData.callback, key = KEY_SECONDARY_GIFT_CARD_COMPONENT, ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardViewModel.kt index 1cacaf07f4..888a2ccf6e 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/giftcard/SessionsGiftCardViewModel.kt @@ -79,7 +79,7 @@ internal class SessionsGiftCardViewModel @Inject constructor( checkoutSession = checkoutSession, paymentMethod = giftCardPaymentMethod, callback = this@SessionsGiftCardViewModel, - ) + ), ) _giftCardViewStateFlow.emit(GiftCardViewState.ShowComponent) } @@ -94,14 +94,15 @@ internal class SessionsGiftCardViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + threeDSMode = keyValueStorage.getThreeDSMode(), redirectUrl = savedStateHandle.get(SessionsGiftCardActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), - installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()) - ) + installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + showRemovePaymentMethodButton = keyValueStorage.isRemoveStoredPaymentMethodEnabled(), + ), ) ?: return null return getCheckoutSession(sessionModel) @@ -114,8 +115,8 @@ internal class SessionsGiftCardViewModel @Inject constructor( return when ( val result = CheckoutSessionProvider.createSession( sessionModel = sessionModel, - configuration = checkoutConfigurationProvider.getGiftCardConfiguration(), - order = order + configuration = checkoutConfigurationProvider.checkoutConfig, + order = order, ) ) { is CheckoutSessionResult.Success -> result.checkoutSession @@ -129,7 +130,7 @@ internal class SessionsGiftCardViewModel @Inject constructor( return when ( val result = CheckoutSessionProvider.createSession( sessionPaymentResult = sessionPaymentResult, - configuration = checkoutConfigurationProvider.getGiftCardConfiguration(), + configuration = checkoutConfigurationProvider.checkoutConfig, ) ) { is CheckoutSessionResult.Success -> result.checkoutSession @@ -169,9 +170,9 @@ internal class SessionsGiftCardViewModel @Inject constructor( _events.emit( GiftCardEvent.ReloadComponentSessions( giftCardComponentData.copy( - checkoutSession = currentSession - ) - ) + checkoutSession = currentSession, + ), + ), ) } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt index 3f28bb0f43..fd943362db 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt @@ -8,13 +8,13 @@ package com.adyen.checkout.example.ui.googlepay +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.googlepay.GooglePayComponentState -import com.adyen.checkout.googlepay.GooglePayConfiguration internal data class GooglePayComponentData( val paymentMethod: PaymentMethod, - val googlePayConfiguration: GooglePayConfiguration, + val checkoutConfiguration: CheckoutConfiguration, val callback: ComponentCallback, ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt index 0184f0a707..91609d18a1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt @@ -81,7 +81,7 @@ class GooglePayFragment : BottomSheetDialogFragment() { GooglePayComponent.PROVIDER.get( fragment = this@GooglePayFragment, paymentMethod = paymentMethod, - configuration = googlePayConfiguration, + checkoutConfiguration = checkoutConfiguration, callback = callback, ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt index 92d4b6d03c..875ba5da16 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.ComponentError @@ -27,7 +28,6 @@ import com.adyen.checkout.example.service.getPaymentMethodRequest import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider import com.adyen.checkout.googlepay.GooglePayComponent import com.adyen.checkout.googlepay.GooglePayComponentState -import com.adyen.checkout.googlepay.GooglePayConfiguration import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -51,7 +51,7 @@ internal class GooglePayViewModel @Inject constructor( ComponentCallback, ComponentAvailableCallback { - private val googlePayConfiguration = checkoutConfigurationProvider.getGooglePayConfiguration() + private val checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig private val _googleComponentDataFlow = MutableStateFlow(null) val googleComponentDataFlow: Flow = _googleComponentDataFlow.filterNotNull() @@ -90,23 +90,23 @@ internal class GooglePayViewModel @Inject constructor( _googleComponentDataFlow.emit( GooglePayComponentData( paymentMethod, - googlePayConfiguration, + checkoutConfiguration, this@GooglePayViewModel, ), ) - checkGooglePayAvailability(paymentMethod, googlePayConfiguration) + checkGooglePayAvailability(paymentMethod, checkoutConfiguration) } private fun checkGooglePayAvailability( paymentMethod: PaymentMethod, - googlePayConfiguration: GooglePayConfiguration, + checkoutConfiguration: CheckoutConfiguration, ) { GooglePayComponent.PROVIDER.isAvailable( - application, - paymentMethod, - googlePayConfiguration, - this, + application = application, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = this, ) } @@ -175,8 +175,7 @@ internal class GooglePayViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(GooglePayFragment.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.kt index 8e46b43d97..dfbbeb55c3 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.kt @@ -8,15 +8,15 @@ package com.adyen.checkout.example.ui.googlepay.compose +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.googlepay.GooglePayComponentState -import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback internal data class SessionsGooglePayComponentData( val checkoutSession: CheckoutSession, - val googlePayConfiguration: GooglePayConfiguration, + val checkoutConfiguration: CheckoutConfiguration, val paymentMethod: PaymentMethod, val callback: SessionComponentCallback ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt index d21bb52097..41a3e65561 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt @@ -183,7 +183,7 @@ private fun getGooglePayComponent(componentData: SessionsGooglePayComponentData) GooglePayComponent.PROVIDER.get( checkoutSession, paymentMethod, - googlePayConfiguration, + checkoutConfiguration, callback, hashCode().toString(), ) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt index 8b7d184ef6..07ceb78c2d 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt @@ -14,6 +14,7 @@ import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentMethod @@ -29,7 +30,6 @@ import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider import com.adyen.checkout.example.ui.googlepay.GooglePayActivityResult import com.adyen.checkout.googlepay.GooglePayComponent import com.adyen.checkout.googlepay.GooglePayComponentState -import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.CheckoutSessionProvider import com.adyen.checkout.sessions.core.CheckoutSessionResult @@ -58,7 +58,7 @@ internal class SessionsGooglePayViewModel @Inject constructor( SessionComponentCallback, ComponentAvailableCallback { - private val googlePayConfiguration = checkoutConfigurationProvider.getGooglePayConfiguration() + private val checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig private val _googlePayState = MutableStateFlow(SessionsGooglePayState(SessionsGooglePayUIState.Loading)) val googlePayState: StateFlow = _googlePayState.asStateFlow() @@ -88,12 +88,12 @@ internal class SessionsGooglePayViewModel @Inject constructor( _componentData = SessionsGooglePayComponentData( checkoutSession, - googlePayConfiguration, + checkoutConfiguration, paymentMethod, this@SessionsGooglePayViewModel, ) - checkGooglePayAvailability(paymentMethod, googlePayConfiguration) + checkGooglePayAvailability(paymentMethod, checkoutConfiguration) } private suspend fun getSession(paymentMethodType: String): CheckoutSession? { @@ -105,25 +105,25 @@ internal class SessionsGooglePayViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + threeDSMode = keyValueStorage.getThreeDSMode(), redirectUrl = savedStateHandle.get(SessionsGooglePayActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), allowedPaymentMethods = listOf(paymentMethodType), installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + showRemovePaymentMethodButton = keyValueStorage.isRemoveStoredPaymentMethodEnabled(), ), ) ?: return null - return getCheckoutSession(sessionModel, googlePayConfiguration) + return getCheckoutSession(sessionModel, checkoutConfiguration) } private suspend fun getCheckoutSession( sessionModel: SessionModel, - googlePayConfiguration: GooglePayConfiguration, + checkoutConfiguration: CheckoutConfiguration, ): CheckoutSession? { - return when (val result = CheckoutSessionProvider.createSession(sessionModel, googlePayConfiguration)) { + return when (val result = CheckoutSessionProvider.createSession(sessionModel, checkoutConfiguration)) { is CheckoutSessionResult.Success -> result.checkoutSession is CheckoutSessionResult.Error -> null } @@ -131,13 +131,13 @@ internal class SessionsGooglePayViewModel @Inject constructor( private fun checkGooglePayAvailability( paymentMethod: PaymentMethod, - googlePayConfiguration: GooglePayConfiguration, + checkoutConfiguration: CheckoutConfiguration, ) { GooglePayComponent.PROVIDER.isAvailable( - application, - paymentMethod, - googlePayConfiguration, - this, + application = application, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + callback = this, ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantEvent.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantEvent.kt index 88678a022a..d9064b1254 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantEvent.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantEvent.kt @@ -15,4 +15,6 @@ internal sealed class InstantEvent { data class PaymentResult(val result: String) : InstantEvent() data class AdditionalAction(val action: Action) : InstantEvent() + + data class PermissionRequest(val requiredPermission: String) : InstantEvent() } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantFragment.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantFragment.kt index fcdc6ae54a..ef71dd936d 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantFragment.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantFragment.kt @@ -14,6 +14,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager @@ -45,6 +46,15 @@ class InstantFragment : BottomSheetDialogFragment() { private val instantViewModel: InstantViewModel by viewModels() + private val requestPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { resultsMap -> + resultsMap.firstNotNullOf { result -> + val requestedPermission = result.key + val isGranted = result.value + instantViewModel.onPermissionResult(requestedPermission, isGranted) + } + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { // Insert return url in extras, so we can access it in the ViewModel through SavedStateHandle val returnUrl = RedirectComponent.getReturnUrl(requireActivity().applicationContext) + RETURN_URL_PATH @@ -75,7 +85,7 @@ class InstantFragment : BottomSheetDialogFragment() { val instantPaymentComponent = InstantPaymentComponent.PROVIDER.get( this, componentData.paymentMethod, - checkoutConfigurationProvider.getInstantConfiguration(), + checkoutConfigurationProvider.checkoutConfig, componentData.callback, ) @@ -83,15 +93,10 @@ class InstantFragment : BottomSheetDialogFragment() { binding.componentView.attach(instantPaymentComponent, viewLifecycleOwner) } - private fun onEvent(event: InstantEvent) { - when (event) { - is InstantEvent.AdditionalAction -> { - onAction(event.action) - } - is InstantEvent.PaymentResult -> { - onPaymentResult(event.result) - } - } + private fun onEvent(event: InstantEvent) = when (event) { + is InstantEvent.AdditionalAction -> onAction(event.action) + is InstantEvent.PaymentResult -> onPaymentResult(event.result) + is InstantEvent.PermissionRequest -> onPermissionRequest(event.requiredPermission) } private fun onPaymentResult(result: String) { @@ -99,6 +104,9 @@ class InstantFragment : BottomSheetDialogFragment() { dismiss() } + private fun onPermissionRequest(requiredPermission: String) = + requestPermissionLauncher.launch(arrayOf(requiredPermission)) + private fun onViewState(viewState: InstantViewState) { when (viewState) { is InstantViewState.Error -> { @@ -107,11 +115,13 @@ class InstantFragment : BottomSheetDialogFragment() { binding.progressIndicator.isVisible = false binding.componentContainer.isVisible = false } + is InstantViewState.Loading -> { binding.progressIndicator.isVisible = true binding.errorView.isVisible = false binding.componentContainer.isVisible = false } + is InstantViewState.ShowComponent -> { binding.progressIndicator.isVisible = false binding.errorView.isVisible = false @@ -140,7 +150,7 @@ class InstantFragment : BottomSheetDialogFragment() { fun show(fragmentManager: FragmentManager, paymentMethodType: String) { InstantFragment().apply { arguments = bundleOf( - PAYMENT_METHOD_TYPE_EXTRA to paymentMethodType + PAYMENT_METHOD_TYPE_EXTRA to paymentMethodType, ) }.show(fragmentManager, TAG) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantViewModel.kt index ecb8caa141..3c547191d5 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/instant/InstantViewModel.kt @@ -16,6 +16,7 @@ import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.ComponentError import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.repositories.PaymentsRepository import com.adyen.checkout.example.service.createPaymentRequest @@ -48,6 +49,8 @@ internal class InstantViewModel @Inject constructor( private val _events = MutableSharedFlow() val events: Flow = _events + private var permissionCallback: PermissionHandlerCallback? = null + init { viewModelScope.launch { fetchPaymentMethods() } } @@ -61,7 +64,7 @@ internal class InstantViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - ) + ), ) val paymentMethod = paymentMethodResponse @@ -71,15 +74,15 @@ internal class InstantViewModel @Inject constructor( if (paymentMethod == null) { _instantViewState.emit( InstantViewState.Error( - "Payment method unavailable, make sure you set the correct country code and currency." - ) + "Payment method unavailable, make sure you set the correct country code and currency.", + ), ) } else { _instantComponentDataFlow.emit( InstantComponentData( paymentMethod = paymentMethod, callback = this@InstantViewModel, - ) + ), ) } } @@ -97,6 +100,24 @@ internal class InstantViewModel @Inject constructor( onComponentError(componentError) } + override fun onPermissionRequest(requiredPermission: String, permissionCallback: PermissionHandlerCallback) { + this.permissionCallback = permissionCallback + + viewModelScope.launch { + _events.emit(InstantEvent.PermissionRequest(requiredPermission)) + } + } + + fun onPermissionResult(requestedPermission: String, isGranted: Boolean) { + if (isGranted) { + permissionCallback?.onPermissionGranted(requestedPermission) + } else { + permissionCallback?.onPermissionDenied(requestedPermission) + } + + permissionCallback = null + } + private fun makePayment(data: PaymentComponentData<*>) { _instantViewState.tryEmit(InstantViewState.Loading) val paymentComponentData = PaymentComponentData.SERIALIZER.serialize(data) @@ -110,8 +131,7 @@ internal class InstantViewModel @Inject constructor( merchantAccount = keyValueStorage.getMerchantAccount(), redirectUrl = savedStateHandle.get(InstantFragment.RETURN_URL_EXTRA) ?: error("Return url should be set"), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + threeDSMode = keyValueStorage.getThreeDSMode(), shopperEmail = keyValueStorage.getShopperEmail(), ) @@ -127,6 +147,7 @@ internal class InstantViewModel @Inject constructor( _instantViewState.tryEmit(InstantViewState.ShowComponent) _events.emit(InstantEvent.AdditionalAction(action)) } + else -> _events.emit(InstantEvent.PaymentResult("Finished: ${json.optString("resultCode")}")) } } ?: _events.emit(InstantEvent.PaymentResult("Failed")) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt index 8d50b61db1..07a9a40bfe 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt @@ -145,7 +145,7 @@ class MainActivity : AppCompatActivity() { this, dropInLauncher, navigation.paymentMethodsApiResponse, - navigation.dropInConfiguration, + navigation.checkoutConfiguration, ExampleAdvancedDropInService::class.java, ) } @@ -155,7 +155,7 @@ class MainActivity : AppCompatActivity() { this, sessionDropInLauncher, navigation.checkoutSession, - navigation.dropInConfiguration, + navigation.checkoutConfiguration, ) } @@ -164,7 +164,7 @@ class MainActivity : AppCompatActivity() { this, sessionDropInLauncher, navigation.checkoutSession, - navigation.dropInConfiguration, + navigation.checkoutConfiguration, ExampleSessionsDropInService::class.java, ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt index e72a8d6ee9..8c3e085919 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt @@ -8,8 +8,8 @@ package com.adyen.checkout.example.ui.main +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodsApiResponse -import com.adyen.checkout.dropin.DropInConfiguration import com.adyen.checkout.sessions.core.CheckoutSession internal sealed class MainNavigation { @@ -36,16 +36,16 @@ internal sealed class MainNavigation { data class DropIn( val paymentMethodsApiResponse: PaymentMethodsApiResponse, - val dropInConfiguration: DropInConfiguration + val checkoutConfiguration: CheckoutConfiguration ) : MainNavigation() data class DropInWithSession( val checkoutSession: CheckoutSession, - val dropInConfiguration: DropInConfiguration + val checkoutConfiguration: CheckoutConfiguration ) : MainNavigation() data class DropInWithCustomSession( val checkoutSession: CheckoutSession, - val dropInConfiguration: DropInConfiguration + val checkoutConfiguration: CheckoutConfiguration ) : MainNavigation() } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt index 980ebc28cf..ccd060ec87 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt @@ -12,7 +12,7 @@ import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.adyen.checkout.dropin.DropInConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.dropin.DropInResult import com.adyen.checkout.dropin.SessionDropInResult import com.adyen.checkout.example.data.storage.KeyValueStorage @@ -125,8 +125,8 @@ internal class MainViewModel @Inject constructor( showLoading(false) if (paymentMethods != null) { - val dropInConfiguration = checkoutConfigurationProvider.getDropInConfiguration() - _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.DropIn(paymentMethods, dropInConfiguration))) + val checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig + _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.DropIn(paymentMethods, checkoutConfiguration))) } else { onError("Something went wrong while fetching payment methods") } @@ -137,17 +137,17 @@ internal class MainViewModel @Inject constructor( viewModelScope.launch { showLoading(true) - val dropInConfiguration = checkoutConfigurationProvider.getDropInConfiguration() + val checkoutConfiguration = checkoutConfigurationProvider.checkoutConfig - val session = getSession(dropInConfiguration) + val session = getSession(checkoutConfiguration) showLoading(false) if (session != null) { val navigation = if (takeOverSession) { - MainNavigation.DropInWithCustomSession(session, dropInConfiguration) + MainNavigation.DropInWithCustomSession(session, checkoutConfiguration) } else { - MainNavigation.DropInWithSession(session, dropInConfiguration) + MainNavigation.DropInWithSession(session, checkoutConfiguration) } _eventFlow.tryEmit(MainEvent.NavigateTo(navigation)) } else { @@ -167,7 +167,7 @@ internal class MainViewModel @Inject constructor( ), ) - private suspend fun getSession(dropInConfiguration: DropInConfiguration): CheckoutSession? { + private suspend fun getSession(checkoutConfiguration: CheckoutConfiguration): CheckoutSession? { val sessionModel = paymentsRepository.createSession( getSessionRequest( merchantAccount = keyValueStorage.getMerchantAccount(), @@ -176,24 +176,24 @@ internal class MainViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - isExecuteThreeD = keyValueStorage.isExecuteThreeD(), - isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + threeDSMode = keyValueStorage.getThreeDSMode(), redirectUrl = savedStateHandle.get(MainActivity.RETURN_URL_EXTRA) ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + showRemovePaymentMethodButton = keyValueStorage.isRemoveStoredPaymentMethodEnabled(), ), ) ?: return null - return getCheckoutSession(sessionModel, dropInConfiguration) + return getCheckoutSession(sessionModel, checkoutConfiguration) } private suspend fun getCheckoutSession( sessionModel: SessionModel, - dropInConfiguration: DropInConfiguration + checkoutConfiguration: CheckoutConfiguration ): CheckoutSession? { - return when (val result = CheckoutSessionProvider.createSession(sessionModel, dropInConfiguration)) { + return when (val result = CheckoutSessionProvider.createSession(sessionModel, checkoutConfiguration)) { is CheckoutSessionResult.Success -> result.checkoutSession is CheckoutSessionResult.Error -> { onError("Something went wrong while starting session") diff --git a/example-app/src/main/res/values/arrays.xml b/example-app/src/main/res/values/arrays.xml index 6d7621546f..bce3f8c802 100644 --- a/example-app/src/main/res/values/arrays.xml +++ b/example-app/src/main/res/values/arrays.xml @@ -12,12 +12,14 @@ "None" "Postal code" "Full address" + "Lookup" "NONE" "POSTAL_CODE" "FULL_ADDRESS" + "LOOKUP" @@ -55,4 +57,16 @@ "ALL" "NONE" + + + "Prefer native" + "Redirect" + "Disabled" + + + + "PREFER_NATIVE" + "REDIRECT" + "DISABLED" + diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index d182683477..a3bfde5856 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -42,10 +42,8 @@ Shopper Information shopper_reference Shopper Reference - threeds2 - 3DS2 - execute_3d - Execute 3D + threeds_mode + 3D Secure shopper_country Country shopper_locale @@ -60,6 +58,8 @@ card_installment_show_amount Show installment amount Address mode + remove_stored_payment_method + Remove stored payment method instant_payment_method_type Instant Payment Method Type use_sessions @@ -80,17 +80,17 @@ Amount (minor units) NL - en-US 1337 EUR - true - true + PREFER_NATIVE false NONE NONE false + true wechatpaySDK true ALL + test-android-components diff --git a/example-app/src/main/res/xml/preferences.xml b/example-app/src/main/res/xml/preferences.xml index 88b84188df..986c71e1b7 100644 --- a/example-app/src/main/res/xml/preferences.xml +++ b/example-app/src/main/res/xml/preferences.xml @@ -33,20 +33,19 @@ android:title="@string/currency_title" app:useSimpleSummaryProvider="true" /> - - - + @@ -58,7 +57,6 @@ app:useSimpleSummaryProvider="true" /> @@ -89,7 +87,7 @@ app:useSimpleSummaryProvider="true" /> @@ -102,6 +100,11 @@ + + diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardComponent.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardComponent.kt index 3a6d993f2d..b4dd8d0e65 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardComponent.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardComponent.kt @@ -22,8 +22,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.giftcard.internal.provider.GiftCardComponentProvider import com.adyen.checkout.giftcard.internal.ui.GiftCardDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate @@ -76,12 +76,13 @@ class GiftCardComponent internal constructor( override fun isConfirmationRequired(): Boolean = giftCardDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? GiftCardDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } /** @@ -93,7 +94,7 @@ class GiftCardComponent internal constructor( */ fun resolveBalanceResult(balanceResult: BalanceResult) { (delegate as? GiftCardDelegate)?.resolveBalanceResult(balanceResult) - ?: Logger.e(TAG, "Payment component is not able to resolve balance result, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not able to resolve balance result, ignoring." } } /** @@ -105,19 +106,18 @@ class GiftCardComponent internal constructor( */ fun resolveOrderResponse(orderResponse: OrderResponse) { (delegate as? GiftCardDelegate)?.resolveOrderResponse(orderResponse) - ?: Logger.e(TAG, "Payment component is not able to resolve order response, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not able to resolve order response, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } giftCardDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = GiftCardComponentProvider() diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt index f8dceb1f37..30c1002d29 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/GiftCardConfiguration.kt @@ -12,9 +12,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -25,7 +28,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class GiftCardConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -45,6 +48,22 @@ class GiftCardConfiguration private constructor( private var isPinRequired: Boolean? = null private var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -52,14 +71,15 @@ class GiftCardConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -68,7 +88,7 @@ class GiftCardConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -110,3 +130,38 @@ class GiftCardConfiguration private constructor( } } } + +fun CheckoutConfiguration.giftCard( + configuration: @CheckoutConfigurationMarker GiftCardConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = GiftCardConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.GIFTCARD, config) + return this +} + +fun CheckoutConfiguration.getGiftCardConfiguration(): GiftCardConfiguration? { + return getConfiguration(PaymentMethodTypes.GIFTCARD) +} + +internal fun GiftCardConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.GIFTCARD, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/GiftCardComponentEventHandler.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/GiftCardComponentEventHandler.kt index 41b07b53db..9658d2b87c 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/GiftCardComponentEventHandler.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/GiftCardComponentEventHandler.kt @@ -3,9 +3,9 @@ package com.adyen.checkout.giftcard.internal import com.adyen.checkout.components.core.internal.BaseComponentCallback import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentCallback import com.adyen.checkout.giftcard.GiftCardComponentState @@ -26,38 +26,42 @@ internal class GiftCardComponentEventHandler : ComponentEventHandler callback.onAdditionalDetails(event.data) is PaymentComponentEvent.Error -> callback.onError(event.error) is PaymentComponentEvent.StateChanged -> callback.onStateChanged(event.state) + is PaymentComponentEvent.PermissionRequest -> callback.onPermissionRequest( + event.requiredPermission, + event.permissionCallback, + ) + is PaymentComponentEvent.Submit -> { when (event.state.giftCardAction) { is GiftCardAction.CheckBalance -> { event.state.data.paymentMethod?.let { callback.onBalanceCheck(event.state) } ?: throw GiftCardException( - "onBalanceCheck cannot be performed due to payment method being null." + "onBalanceCheck cannot be performed due to payment method being null.", ) } + is GiftCardAction.SendPayment -> { callback.onSubmit(event.state) } + is GiftCardAction.CreateOrder -> { callback.onRequestOrder() } + is GiftCardAction.Idle -> { // no ops - Logger.d(TAG, "No action to be taken.") + adyenLog(AdyenLogLevel.DEBUG) { "No action to be taken." } } } } } } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/SessionsGiftCardComponentEventHandler.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/SessionsGiftCardComponentEventHandler.kt index 824c26ab4b..fe9a32b85b 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/SessionsGiftCardComponentEventHandler.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/SessionsGiftCardComponentEventHandler.kt @@ -15,9 +15,10 @@ import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.internal.BaseComponentCallback import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.GiftCardException @@ -52,7 +53,7 @@ class SessionsGiftCardComponentEventHandler( } private fun updateSessionData(sessionData: String) { - Logger.v(TAG, "Updating session data - $sessionData") + adyenLog(AdyenLogLevel.VERBOSE) { "Updating session data - $sessionData" } sessionSavedStateHandleContainer.updateSessionData(sessionData) } @@ -60,14 +61,19 @@ class SessionsGiftCardComponentEventHandler( event: PaymentComponentEvent, componentCallback: BaseComponentCallback ) { - @Suppress("UNCHECKED_CAST") val sessionComponentCallback = componentCallback as? SessionsGiftCardComponentCallback ?: throw CheckoutException("Callback must be type of ${SessionComponentCallback::class.java.canonicalName}") - Logger.v(TAG, "Event received $event") + adyenLog(AdyenLogLevel.VERBOSE) { "Event received $event" } when (event) { is PaymentComponentEvent.ActionDetails -> onDetailsCallRequested(event.data, sessionComponentCallback) is PaymentComponentEvent.Error -> onComponentError(event.error, sessionComponentCallback) is PaymentComponentEvent.StateChanged -> onState(event.state, sessionComponentCallback) + is PaymentComponentEvent.PermissionRequest -> onPermissionRequest( + event.requiredPermission, + event.permissionCallback, + sessionComponentCallback, + ) + is PaymentComponentEvent.Submit -> { when (event.state.giftCardAction) { GiftCardAction.CheckBalance -> { @@ -75,6 +81,7 @@ class SessionsGiftCardComponentEventHandler( checkBalance(event.state, sessionComponentCallback) } ?: throw GiftCardException("Payment method is null.") } + GiftCardAction.CreateOrder -> createOrder(sessionComponentCallback) GiftCardAction.SendPayment -> onPaymentsCallRequested(event.state, sessionComponentCallback) GiftCardAction.Idle -> {} // no ops @@ -98,15 +105,18 @@ class SessionsGiftCardComponentEventHandler( is SessionCallResult.Payments.Action -> { sessionComponentCallback.onAction(result.action) } + is SessionCallResult.Payments.Error -> onSessionError(result.throwable, sessionComponentCallback) is SessionCallResult.Payments.Finished -> onFinished(result.result, sessionComponentCallback) is SessionCallResult.Payments.NotFullyPaidOrder -> { onPartialPayment(result, sessionComponentCallback) } + is SessionCallResult.Payments.RefusedPartialPayment -> onFinished( result.result, - sessionComponentCallback + sessionComponentCallback, ) + is SessionCallResult.Payments.TakenOver -> { setFlowTakenOver() } @@ -122,13 +132,14 @@ class SessionsGiftCardComponentEventHandler( val result = sessionInteractor.onDetailsCallRequested( actionComponentData, sessionComponentCallback::onAdditionalDetails, - sessionComponentCallback::onAdditionalDetails.name + sessionComponentCallback::onAdditionalDetails.name, ) when (result) { is SessionCallResult.Details.Action -> { sessionComponentCallback.onAction(result.action) } + is SessionCallResult.Details.Error -> onSessionError(result.throwable, sessionComponentCallback) is SessionCallResult.Details.Finished -> onFinished(result.result, sessionComponentCallback) SessionCallResult.Details.TakenOver -> { @@ -153,6 +164,7 @@ class SessionsGiftCardComponentEventHandler( is SessionCallResult.Balance.Successful -> { sessionComponentCallback.onBalance(result.balanceResult) } + SessionCallResult.Balance.TakenOver -> { setFlowTakenOver() } @@ -164,7 +176,7 @@ class SessionsGiftCardComponentEventHandler( coroutineScope.launchWithLoadingState(sessionComponentCallback) { val result = sessionInteractor.createOrder( sessionComponentCallback::onOrderRequest, - sessionComponentCallback::onOrderRequest.name + sessionComponentCallback::onOrderRequest.name, ) when (result) { @@ -172,6 +184,7 @@ class SessionsGiftCardComponentEventHandler( is SessionCallResult.CreateOrder.Successful -> { sessionComponentCallback.onOrder(result.order) } + SessionCallResult.CreateOrder.TakenOver -> { setFlowTakenOver() } @@ -197,6 +210,14 @@ class SessionsGiftCardComponentEventHandler( sessionComponentCallback.onStateChanged(state) } + private fun onPermissionRequest( + requiredPermission: String, + permissionCallback: PermissionHandlerCallback, + sessionComponentCallback: SessionsGiftCardComponentCallback + ) { + sessionComponentCallback.onPermissionRequest(requiredPermission, permissionCallback) + } + private fun onComponentError( error: ComponentError, sessionComponentCallback: SessionsGiftCardComponentCallback @@ -210,8 +231,8 @@ class SessionsGiftCardComponentEventHandler( ) { sessionComponentCallback.onError( ComponentError( - CheckoutException(throwable.message.orEmpty(), throwable) - ) + CheckoutException(throwable.message.orEmpty(), throwable), + ), ) } @@ -219,7 +240,7 @@ class SessionsGiftCardComponentEventHandler( result: SessionPaymentResult, sessionComponentCallback: SessionsGiftCardComponentCallback ) { - Logger.d(TAG, "Finished: ${result.resultCode}") + adyenLog(AdyenLogLevel.DEBUG) { "Finished: ${result.resultCode}" } sessionComponentCallback.onFinished(result) } @@ -227,21 +248,17 @@ class SessionsGiftCardComponentEventHandler( sessionCallResult: SessionCallResult.Payments.NotFullyPaidOrder, sessionComponentCallback: SessionsGiftCardComponentCallback ) { - Logger.d(TAG, "Partial payment: ${sessionCallResult.result.order}") + adyenLog(AdyenLogLevel.DEBUG) { "Partial payment: ${sessionCallResult.result.order}" } sessionComponentCallback.onPartialPayment(sessionCallResult.result) } private fun setFlowTakenOver() { if (sessionSavedStateHandleContainer.isFlowTakenOver == true) return sessionSavedStateHandleContainer.isFlowTakenOver = true - Logger.i(TAG, "Flow was taken over.") + adyenLog(AdyenLogLevel.INFO) { "Flow was taken over." } } override fun onCleared() { _coroutineScope = null } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt index 3346453c19..85ec671395 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository @@ -26,12 +27,13 @@ import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepo import com.adyen.checkout.components.core.internal.data.api.DefaultPublicKeyRepository import com.adyen.checkout.components.core.internal.data.api.PublicKeyService import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.cse.internal.CardEncryptorFactory import com.adyen.checkout.giftcard.GiftCardComponent import com.adyen.checkout.giftcard.GiftCardComponentCallback @@ -43,6 +45,7 @@ import com.adyen.checkout.giftcard.internal.SessionsGiftCardComponentCallbackWra import com.adyen.checkout.giftcard.internal.SessionsGiftCardComponentEventHandler import com.adyen.checkout.giftcard.internal.ui.DefaultGiftCardDelegate import com.adyen.checkout.giftcard.internal.ui.model.GiftCardComponentParamsMapper +import com.adyen.checkout.giftcard.toCheckoutConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.internal.SessionInteractor import com.adyen.checkout.sessions.core.internal.SessionSavedStateHandleContainer @@ -55,31 +58,29 @@ import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class GiftCardComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< GiftCardComponent, GiftCardConfiguration, GiftCardComponentState, - GiftCardComponentCallback + GiftCardComponentCallback, >, SessionPaymentComponentProvider< GiftCardComponent, GiftCardConfiguration, GiftCardComponentState, - SessionsGiftCardComponentCallback + SessionsGiftCardComponentCallback, > { - private val componentParamsMapper = GiftCardComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: GiftCardConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: GiftCardComponentCallback, order: Order?, @@ -89,7 +90,13 @@ constructor( val cardEncryptor = CardEncryptorFactory.provide() val giftCardFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GiftCardComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val publicKeyService = PublicKeyService(httpClient) @@ -100,7 +107,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -116,8 +123,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -138,6 +145,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: GiftCardConfiguration, + application: Application, + componentCallback: GiftCardComponentCallback, + order: Order?, + key: String?, + ): GiftCardComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -145,7 +176,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: GiftCardConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionsGiftCardComponentCallback, key: String? @@ -154,10 +185,13 @@ constructor( val cardEncryptor = CardEncryptorFactory.provide() val giftCardFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = GiftCardComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val publicKeyService = PublicKeyService(httpClient) @@ -169,7 +203,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -185,8 +219,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -202,7 +236,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionsGiftCardComponentEventHandler = SessionsGiftCardComponentEventHandler( @@ -222,7 +256,7 @@ constructor( .also { component -> val internalComponentCallback = SessionsGiftCardComponentCallbackWrapper( component, - componentCallback + componentCallback, ) component.observe(lifecycleOwner) { component.componentEventHandler.onPaymentComponentEvent(it, internalComponentCallback) @@ -230,6 +264,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: GiftCardConfiguration, + application: Application, + componentCallback: SessionsGiftCardComponentCallback, + key: String? + ): GiftCardComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt index 5eff1d99b8..da2611d89e 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt @@ -25,10 +25,10 @@ import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.GiftCardPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.cse.EncryptedCard import com.adyen.checkout.cse.EncryptionException import com.adyen.checkout.cse.UnencryptedCard @@ -100,28 +100,28 @@ internal class DefaultGiftCardDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } } private fun fetchPublicKey(coroutineScope: CoroutineScope) { - Logger.d(TAG, "fetchPublicKey") + adyenLog(AdyenLogLevel.DEBUG) { "fetchPublicKey" } coroutineScope.launch { publicKeyRepository.fetchPublicKey( environment = componentParams.environment, - clientKey = componentParams.clientKey + clientKey = componentParams.clientKey, ).fold( onSuccess = { key -> - Logger.d(TAG, "Public key fetched") + adyenLog(AdyenLogLevel.DEBUG) { "Public key fetched" } publicKey = key updateComponentState(outputData) }, onFailure = { e -> - Logger.e(TAG, "Unable to fetch public key") + adyenLog(AdyenLogLevel.ERROR) { "Unable to fetch public key" } exceptionChannel.trySend(ComponentException("Unable to fetch publicKey.", e)) - } + }, ) } } @@ -137,7 +137,7 @@ internal class DefaultGiftCardDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -186,7 +186,7 @@ internal class DefaultGiftCardDelegate( isInputValid = outputData.isValid, isReady = false, lastFourDigits = null, - giftCardAction = GiftCardAction.Idle + giftCardAction = GiftCardAction.Idle, ) if (!outputData.isValid) { @@ -195,7 +195,7 @@ internal class DefaultGiftCardDelegate( isInputValid = false, isReady = true, lastFourDigits = null, - giftCardAction = GiftCardAction.Idle + giftCardAction = GiftCardAction.Idle, ) } @@ -204,7 +204,7 @@ internal class DefaultGiftCardDelegate( isInputValid = false, isReady = true, lastFourDigits = null, - giftCardAction = GiftCardAction.Idle + giftCardAction = GiftCardAction.Idle, ) val giftCardPaymentMethod = GiftCardPaymentMethod( @@ -228,7 +228,7 @@ internal class DefaultGiftCardDelegate( isInputValid = true, isReady = true, lastFourDigits = lastDigits, - giftCardAction = GiftCardAction.CheckBalance + giftCardAction = GiftCardAction.CheckBalance, ) } @@ -271,7 +271,7 @@ internal class DefaultGiftCardDelegate( val balanceStatus = GiftCardBalanceUtils.checkBalance( balance = balanceResult.balance, transactionLimit = balanceResult.transactionLimit, - amountToBePaid = componentParams.amount + amountToBePaid = componentParams.amount, ) resolveBalanceStatus(balanceStatus) @@ -283,7 +283,7 @@ internal class DefaultGiftCardDelegate( when (balanceStatus) { is GiftCardBalanceStatus.FullPayment -> { val updatedState = currentState.copy( - giftCardAction = GiftCardAction.SendPayment + giftCardAction = GiftCardAction.SendPayment, ) _componentStateFlow.tryEmit(updatedState) submitHandler.onSubmit(updatedState) @@ -291,7 +291,7 @@ internal class DefaultGiftCardDelegate( is GiftCardBalanceStatus.NonMatchingCurrencies -> { exceptionChannel.trySend( - GiftCardException("Currency of the gift card does not match the currency of transaction.") + GiftCardException("Currency of the gift card does not match the currency of transaction."), ) } @@ -302,8 +302,8 @@ internal class DefaultGiftCardDelegate( currentState.copy( giftCardAction = GiftCardAction.SendPayment, data = currentState.data.copy( - amount = balanceStatus.amountPaid - ) + amount = balanceStatus.amountPaid, + ), ) } cachedAmount = balanceStatus.amountPaid @@ -313,13 +313,13 @@ internal class DefaultGiftCardDelegate( is GiftCardBalanceStatus.ZeroAmountToBePaid -> { exceptionChannel.trySend( - GiftCardException("Amount of the transaction is zero.") + GiftCardException("Amount of the transaction is zero."), ) } is GiftCardBalanceStatus.ZeroBalance -> { exceptionChannel.trySend( - GiftCardException("Gift card has no balance.") + GiftCardException("Gift card has no balance."), ) } } @@ -332,10 +332,10 @@ internal class DefaultGiftCardDelegate( data = currentState.data.copy( order = OrderRequest( orderData = orderResponse.orderData, - pspReference = orderResponse.pspReference + pspReference = orderResponse.pspReference, ), - amount = cachedAmount - ) + amount = cachedAmount, + ), ) cachedAmount = null _componentStateFlow.tryEmit(updatedState) @@ -349,7 +349,6 @@ internal class DefaultGiftCardDelegate( } companion object { - private val TAG = LogUtil.getTag() private const val LAST_DIGITS_LENGTH = 4 } } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt index 4bfbaa3b0b..49d3df4d47 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParams.kt @@ -8,20 +8,12 @@ package com.adyen.checkout.giftcard.internal.ui.model -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment -import java.util.Locale internal data class GiftCardComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, val isPinRequired: Boolean, -) : ComponentParams, ButtonParams +) : ComponentParams by commonComponentParams, ButtonParams diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt index 15b823a712..c98065711d 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapper.kt @@ -8,60 +8,36 @@ package com.adyen.checkout.giftcard.internal.ui.model -import com.adyen.checkout.components.core.internal.ButtonConfiguration -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams -import com.adyen.checkout.giftcard.GiftCardConfiguration +import com.adyen.checkout.giftcard.getGiftCardConfiguration +import java.util.Locale internal class GiftCardComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - configuration: GiftCardConfiguration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, ): GiftCardComponentParams { - return configuration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } - - private fun GiftCardConfiguration.mapToParamsInternal(): GiftCardComponentParams { - return GiftCardComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - isSubmitButtonVisible = (this as? ButtonConfiguration)?.isSubmitButtonVisible ?: true, - isPinRequired = isPinRequired ?: true, - ) - } - - private fun GiftCardComponentParams.override( - overrideComponentParams: ComponentParams? - ): GiftCardComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, ) - } + val commonComponentParams = commonComponentParamsMapperData.commonComponentParams + val giftCardConfiguration = checkoutConfiguration.getGiftCardConfiguration() - private fun GiftCardComponentParams.override( - sessionParams: SessionParams? = null - ): GiftCardComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, + return GiftCardComponentParams( + commonComponentParams = commonComponentParams, + isSubmitButtonVisible = giftCardConfiguration?.isSubmitButtonVisible ?: true, + isPinRequired = giftCardConfiguration?.isPinRequired ?: true, ) } } diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt index 0ba8c00417..9b6bdd3403 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/view/GiftCardView.kt @@ -16,8 +16,8 @@ import android.view.View.OnFocusChangeListener import android.widget.LinearLayout import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.giftcard.R import com.adyen.checkout.giftcard.databinding.GiftcardViewBinding import com.adyen.checkout.giftcard.internal.ui.GiftCardDelegate @@ -36,7 +36,7 @@ internal class GiftCardView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -64,7 +64,7 @@ internal class GiftCardView @JvmOverloads constructor( private fun initCardNumberField(localizedContext: Context) { binding.textInputLayoutGiftcardNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_GiftCard_GiftCardNumberInput, - localizedContext + localizedContext, ) binding.editTextGiftcardNumber.setOnChangeListener { @@ -86,7 +86,7 @@ internal class GiftCardView @JvmOverloads constructor( if (giftCardDelegate.isPinRequired()) { binding.textInputLayoutGiftcardPin.setLocalizedHintFromStyle( R.style.AdyenCheckout_GiftCard_GiftCardPinInput, - localizedContext + localizedContext, ) binding.editTextGiftcardPin.setOnChangeListener { editable: Editable -> @@ -108,7 +108,7 @@ internal class GiftCardView @JvmOverloads constructor( } override fun highlightValidationErrors() { - Logger.d(TAG, "highlightValidationErrors") + adyenLog(AdyenLogLevel.DEBUG) { "highlightValidationErrors" } val outputData = giftCardDelegate.outputData var isErrorFocused = false val cardNumberValidation = outputData.numberFieldState.validation @@ -127,8 +127,4 @@ internal class GiftCardView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardComponentTest.kt b/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardComponentTest.kt index 7cab73cc15..a643e21db5 100644 --- a/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardComponentTest.kt +++ b/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardComponentTest.kt @@ -17,10 +17,9 @@ import com.adyen.checkout.components.core.BalanceResult import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.giftcard.internal.ui.GiftCardComponentViewType import com.adyen.checkout.giftcard.internal.ui.GiftCardDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -42,7 +41,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class GiftCardComponentTest( @Mock private val giftCardDelegate: GiftCardDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -61,9 +60,8 @@ internal class GiftCardComponentTest( giftCardDelegate = giftCardDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -117,7 +115,7 @@ internal class GiftCardComponentTest( giftCardDelegate = giftCardDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) component.viewFlow.test { @@ -138,7 +136,7 @@ internal class GiftCardComponentTest( giftCardDelegate = giftCardDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) component.viewFlow.test { @@ -204,14 +202,14 @@ internal class GiftCardComponentTest( companion object { private val BALANCE_RESULT = BalanceResult( balance = null, - transactionLimit = null + transactionLimit = null, ) private val ORDER_RESPONSE = OrderResponse( pspReference = "psp", orderData = "orderData", amount = null, - remainingAmount = null + remainingAmount = null, ) } } diff --git a/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardConfigurationTest.kt b/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardConfigurationTest.kt new file mode 100644 index 0000000000..9cb5398c7f --- /dev/null +++ b/giftcard/src/test/java/com/adyen/checkout/giftcard/GiftCardConfigurationTest.kt @@ -0,0 +1,93 @@ +package com.adyen.checkout.giftcard + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class GiftCardConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + giftCard { + setSubmitButtonVisible(false) + setPinRequired(false) + } + } + + val actual = checkoutConfiguration.getGiftCardConfiguration() + + val expected = GiftCardConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .setPinRequired(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + assertEquals(expected.isPinRequired, actual?.isPinRequired) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = GiftCardConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .setPinRequired(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualGiftCardConfig = actual.getGiftCardConfiguration() + assertEquals(config.shopperLocale, actualGiftCardConfig?.shopperLocale) + assertEquals(config.environment, actualGiftCardConfig?.environment) + assertEquals(config.clientKey, actualGiftCardConfig?.clientKey) + assertEquals(config.amount, actualGiftCardConfig?.amount) + assertEquals(config.analyticsConfiguration, actualGiftCardConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualGiftCardConfig?.isSubmitButtonVisible) + assertEquals(config.isPinRequired, actualGiftCardConfig?.isPinRequired) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt index 0b080db0a1..0a6ad6e5e5 100644 --- a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt +++ b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt @@ -10,18 +10,21 @@ package com.adyen.checkout.giftcard.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.OrderResponse import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment import com.adyen.checkout.cse.internal.test.TestCardEncryptor import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.GiftCardConfiguration import com.adyen.checkout.giftcard.GiftCardException +import com.adyen.checkout.giftcard.giftCard import com.adyen.checkout.giftcard.internal.ui.model.GiftCardComponentParamsMapper import com.adyen.checkout.giftcard.internal.ui.model.GiftCardOutputData import com.adyen.checkout.giftcard.internal.util.GiftCardBalanceStatus @@ -162,9 +165,7 @@ internal class DefaultGiftCardDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultGiftCardConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createGiftCardDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -190,9 +191,9 @@ internal class DefaultGiftCardDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createGiftCardDelegate( - configuration = getDefaultGiftCardConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -201,9 +202,9 @@ internal class DefaultGiftCardDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createGiftCardDelegate( - configuration = getDefaultGiftCardConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -253,7 +254,7 @@ internal class DefaultGiftCardDelegateTest( fun `when balance result is FullPayment then giftCardAction should be SendPayment`() = runTest { val fullPaymentBalanceStatus = GiftCardBalanceStatus.FullPayment( amountPaid = Amount(value = 50_00, currency = "EUR"), - remainingBalance = Amount(value = 0L, currency = "EUR") + remainingBalance = Amount(value = 0L, currency = "EUR"), ) delegate.resolveBalanceStatus(fullPaymentBalanceStatus) @@ -271,7 +272,7 @@ internal class DefaultGiftCardDelegateTest( delegate = createGiftCardDelegate(order = null) val partialPaymentBalanceStatus = GiftCardBalanceStatus.PartialPayment( amountPaid = Amount(value = 50_00, currency = "EUR"), - remainingBalance = Amount(value = 0L, currency = "EUR") + remainingBalance = Amount(value = 0L, currency = "EUR"), ) delegate.resolveBalanceStatus(partialPaymentBalanceStatus) @@ -288,7 +289,7 @@ internal class DefaultGiftCardDelegateTest( runTest { val partialPaymentBalanceStatus = GiftCardBalanceStatus.PartialPayment( amountPaid = Amount(value = 50_00, currency = "EUR"), - remainingBalance = Amount(value = 0L, currency = "EUR") + remainingBalance = Amount(value = 0L, currency = "EUR"), ) delegate.resolveBalanceStatus(partialPaymentBalanceStatus) @@ -340,7 +341,7 @@ internal class DefaultGiftCardDelegateTest( pspReference = "test_psp", orderData = "test_order_data", amount = null, - remainingAmount = null + remainingAmount = null, ) delegate.resolveOrderResponse(orderResponse) delegate.componentStateFlow.test { @@ -348,7 +349,7 @@ internal class DefaultGiftCardDelegateTest( val expectedOrderRequest = OrderRequest( orderData = "test_order_data", - pspReference = "test_psp" + pspReference = "test_psp", ) assertEquals(expectedOrderRequest, state.data.order) assertEquals(GiftCardAction.SendPayment, state.giftCardAction) @@ -379,7 +380,9 @@ internal class DefaultGiftCardDelegateTest( @Test fun `when pin is not required, then does not matter for validation`() = runTest { delegate = createGiftCardDelegate( - configuration = getDefaultGiftCardConfigurationBuilder().setPinRequired(false).build() + configuration = createCheckoutConfiguration { + setPinRequired(false) + }, ) delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) val componentStateFlow = delegate.componentStateFlow.test(testScheduler) @@ -395,26 +398,36 @@ internal class DefaultGiftCardDelegateTest( } private fun createGiftCardDelegate( - configuration: GiftCardConfiguration = getDefaultGiftCardConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: OrderRequest? = TEST_ORDER ) = DefaultGiftCardDelegate( observerRepository = PaymentObserverRepository(), paymentMethod = PaymentMethod(), order = order, publicKeyRepository = publicKeyRepository, - componentParams = GiftCardComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = GiftCardComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), cardEncryptor = cardEncryptor, analyticsRepository = analyticsRepository, submitHandler = submitHandler, ) - private fun getDefaultGiftCardConfigurationBuilder() = GiftCardConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: GiftCardConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + giftCard(configuration) + } - private fun giftCardOutputDataWith(number: String, pin: String) = GiftCardOutputData( + private fun giftCardOutputDataWith( + number: String, + @Suppress("SameParameterValue") pin: String, + ) = GiftCardOutputData( numberFieldState = GiftCardNumberUtils.validateInputField(number), pinFieldState = GiftCardPinUtils.validateInputField(pin), ) diff --git a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapperTest.kt b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapperTest.kt new file mode 100644 index 0000000000..43d6097f6e --- /dev/null +++ b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/model/GiftCardComponentParamsMapperTest.kt @@ -0,0 +1,243 @@ +package com.adyen.checkout.giftcard.internal.ui.model + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration +import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.core.Environment +import com.adyen.checkout.giftcard.GiftCardConfiguration +import com.adyen.checkout.giftcard.giftCard +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.Locale + +internal class GiftCardComponentParamsMapperTest { + + private val giftCardComponentParamsMapper = GiftCardComponentParamsMapper(CommonComponentParamsMapper()) + + @Test + fun `when drop-in override params are null then params should match the component configuration`() { + val configuration = createCheckoutConfiguration { + setPinRequired(false) + setSubmitButtonVisible(false) + } + + val params = giftCardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, null, null) + + val expected = getComponentParams( + isPinRequired = false, + isSubmitButtonVisible = false, + ) + + assertEquals(expected, params) + } + + @Test + fun `when drop-in override params are set then they should override component configuration fields`() { + val configuration = CheckoutConfiguration( + shopperLocale = Locale.GERMAN, + environment = Environment.EUROPE, + clientKey = TEST_CLIENT_KEY_2, + amount = Amount( + currency = "EUR", + value = 49_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + giftCard { + setAmount(Amount("USD", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + } + } + + val dropInOverrideParams = DropInOverrideParams(Amount("CAD", 123L), null) + val params = giftCardComponentParamsMapper.mapToParams(configuration, DEVICE_LOCALE, dropInOverrideParams, null) + + val expected = getComponentParams( + shopperLocale = Locale.GERMAN, + environment = Environment.EUROPE, + clientKey = TEST_CLIENT_KEY_2, + analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), + isCreatedByDropIn = true, + amount = Amount( + currency = "CAD", + value = 123L, + ), + isPinRequired = true, + isSubmitButtonVisible = true, + ) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("amountSource") + fun `amount should match value set in sessions then drop in then component configuration`( + configurationValue: Amount, + dropInValue: Amount?, + sessionsValue: Amount?, + expectedValue: Amount + ) { + val testConfiguration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + + val params = giftCardComponentParamsMapper.mapToParams( + checkoutConfiguration = testConfiguration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + ) + + val expected = getComponentParams(amount = expectedValue, isCreatedByDropIn = dropInOverrideParams != null) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = giftCardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getComponentParams( + shopperLocale = expectedValue, + ) + + assertEquals(expected, params) + } + + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = giftCardComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + ) + + val expected = getComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: GiftCardConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + giftCard(configuration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, + ) + + @Suppress("LongParameterList") + private fun getComponentParams( + shopperLocale: Locale = DEVICE_LOCALE, + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + isCreatedByDropIn: Boolean = false, + amount: Amount? = null, + isSubmitButtonVisible: Boolean = true, + isPinRequired: Boolean = true, + ): GiftCardComponentParams { + return GiftCardComponentParams( + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), + isSubmitButtonVisible = isSubmitButtonVisible, + isPinRequired = isPinRequired, + ) + } + + companion object { + private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") + + @JvmStatic + fun amountSource() = listOf( + // configurationValue, dropInValue, sessionsValue, expectedValue + arguments(Amount("EUR", 100), Amount("USD", 200), Amount("CAD", 300), Amount("CAD", 300)), + arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), + arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), + ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) + } +} diff --git a/googlepay/build.gradle b/googlepay/build.gradle index f4fd4461b7..be28dc94a0 100644 --- a/googlepay/build.gradle +++ b/googlepay/build.gradle @@ -36,6 +36,7 @@ android { dependencies { // Checkout + api project(':3ds2') api project(':action-core') api project(':components-core') api project(':sessions-core') diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt index c1fe1db0e2..f484ff2b2b 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayComponent.kt @@ -22,8 +22,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.googlepay.internal.provider.GooglePayComponentProvider import com.adyen.checkout.googlepay.internal.ui.GooglePayDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -97,19 +97,18 @@ class GooglePayComponent internal constructor( } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { - Logger.w(TAG, "Interaction with GooglePayComponent can't be blocked") + adyenLog(AdyenLogLevel.WARN) { "Interaction with GooglePayComponent can't be blocked" } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } googlePayDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = GooglePayComponentProvider() diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt index 7bd14475f9..53827e4b6e 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/GooglePayConfiguration.kt @@ -14,8 +14,10 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.CheckoutException import com.google.android.gms.wallet.WalletConstants @@ -28,7 +30,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class GooglePayConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -75,6 +77,22 @@ class GooglePayConfiguration private constructor( private var billingAddressParameters: BillingAddressParameters? = null private var totalPriceStatus: String? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -82,14 +100,15 @@ class GooglePayConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -127,7 +146,7 @@ class GooglePayConfiguration private constructor( if (!isGooglePayEnvironmentValid(googlePayEnvironment)) { throw CheckoutException( "Invalid value for Google Environment. Use either WalletConstants.ENVIRONMENT_TEST or" + - " WalletConstants.ENVIRONMENT_PRODUCTION" + " WalletConstants.ENVIRONMENT_PRODUCTION", ) } this.googlePayEnvironment = googlePayEnvironment @@ -348,6 +367,10 @@ class GooglePayConfiguration private constructor( * [Google Pay docs](https://developers.google.com/pay/api/android/reference/request-objects#TransactionInfo) * for more details. * + * Not applicable for the sessions flow. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * * @param amount Amount of the transaction. */ @Suppress("RedundantOverride") @@ -383,3 +406,46 @@ class GooglePayConfiguration private constructor( } } } + +fun CheckoutConfiguration.googlePay( + configuration: @CheckoutConfigurationMarker GooglePayConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = GooglePayConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + + GooglePayComponent.PAYMENT_METHOD_TYPES.forEach { key -> + addConfiguration(key, config) + } + + return this +} + +fun CheckoutConfiguration.getGooglePayConfiguration(): GooglePayConfiguration? { + return GooglePayComponent.PAYMENT_METHOD_TYPES.firstNotNullOfOrNull { key -> + getConfiguration(key) + } +} + +internal fun GooglePayConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + GooglePayComponent.PAYMENT_METHOD_TYPES.forEach { key -> + addConfiguration(key, this@toCheckoutConfiguration) + } + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/provider/GooglePayComponentProvider.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/provider/GooglePayComponentProvider.kt index 9d8471b448..a308067728 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/provider/GooglePayComponentProvider.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/provider/GooglePayComponentProvider.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentAvailableCallback import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order @@ -28,21 +29,23 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryD import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.LocaleProvider +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.googlepay.GooglePayComponent import com.adyen.checkout.googlepay.GooglePayComponentState import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.googlepay.internal.ui.DefaultGooglePayDelegate import com.adyen.checkout.googlepay.internal.ui.model.GooglePayComponentParamsMapper import com.adyen.checkout.googlepay.internal.util.GooglePayUtils +import com.adyen.checkout.googlepay.toCheckoutConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -60,42 +63,45 @@ import java.lang.ref.WeakReference class GooglePayComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< GooglePayComponent, GooglePayConfiguration, GooglePayComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< GooglePayComponent, GooglePayConfiguration, GooglePayComponentState, - SessionComponentCallback + SessionComponentCallback, >, PaymentMethodAvailabilityCheck { - private val componentParamsMapper = GooglePayComponentParamsMapper(overrideComponentParams, overrideSessionParams) - @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: GooglePayConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, key: String?, ): GooglePayComponent { assertSupported(paymentMethod) - - val componentParams = componentParamsMapper.mapToParams(configuration, paymentMethod, null) val googlePayFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + val componentParams = GooglePayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = paymentMethod, + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -104,7 +110,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -117,8 +123,8 @@ constructor( analyticsRepository = analyticsRepository, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -144,21 +150,48 @@ constructor( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, - checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, configuration: GooglePayConfiguration, application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String?, + ): GooglePayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + + @Suppress("LongMethod") + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, componentCallback: SessionComponentCallback, key: String? ): GooglePayComponent { assertSupported(paymentMethod) - - val componentParams = componentParamsMapper.mapToParams( - googlePayConfiguration = configuration, - paymentMethod = paymentMethod, - sessionParams = SessionParamsFactory.create(checkoutSession), - ) val googlePayFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> + val componentParams = GooglePayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + paymentMethod = paymentMethod, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -169,7 +202,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -182,8 +215,8 @@ constructor( analyticsRepository = analyticsRepository, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -199,7 +232,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( @@ -223,43 +256,86 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: GooglePayConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): GooglePayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + override fun isAvailable( - applicationContext: Application, + application: Application, paymentMethod: PaymentMethod, - configuration: GooglePayConfiguration?, + checkoutConfiguration: CheckoutConfiguration, callback: ComponentAvailableCallback ) { - if (configuration == null) { - throw CheckoutException("GooglePayConfiguration cannot be null") - } if ( GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(applicationContext) != ConnectionResult.SUCCESS + .isGooglePlayServicesAvailable(application) != ConnectionResult.SUCCESS ) { callback.onAvailabilityResult(false, paymentMethod) return } val callbackWeakReference = WeakReference(callback) - val componentParams = componentParamsMapper.mapToParams(configuration, paymentMethod, null) - val paymentsClient = Wallet.getPaymentsClient( - applicationContext, - GooglePayUtils.createWalletOptions(componentParams) + val componentParams = GooglePayComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = paymentMethod, ) + + val paymentsClient = Wallet.getPaymentsClient(application, GooglePayUtils.createWalletOptions(componentParams)) val readyToPayRequest = GooglePayUtils.createIsReadyToPayRequest(componentParams) val readyToPayTask = paymentsClient.isReadyToPay(readyToPayRequest) readyToPayTask.addOnSuccessListener { result -> callbackWeakReference.get()?.onAvailabilityResult(result == true, paymentMethod) } readyToPayTask.addOnCanceledListener { - Logger.e(TAG, "GooglePay readyToPay task is cancelled.") + adyenLog(AdyenLogLevel.ERROR) { "GooglePay readyToPay task is cancelled." } callbackWeakReference.get()?.onAvailabilityResult(false, paymentMethod) } readyToPayTask.addOnFailureListener { - Logger.e(TAG, "GooglePay readyToPay task is failed.", it) + adyenLog(AdyenLogLevel.ERROR, it) { "GooglePay readyToPay task is failed." } callbackWeakReference.get()?.onAvailabilityResult(false, paymentMethod) } } + override fun isAvailable( + applicationContext: Application, + paymentMethod: PaymentMethod, + configuration: GooglePayConfiguration?, + callback: ComponentAvailableCallback + ) { + if (configuration == null) { + throw CheckoutException("GooglePayConfiguration cannot be null") + } + + isAvailable( + application = applicationContext, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") @@ -269,8 +345,4 @@ constructor( override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean { return GooglePayComponent.PAYMENT_METHOD_TYPES.contains(paymentMethod.type) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt index 7a38258542..8ab1f14270 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegate.kt @@ -20,11 +20,11 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.util.bufferedChannel +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.model.ModelUtils -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.googlepay.GooglePayButtonParameters import com.adyen.checkout.googlepay.GooglePayComponentState import com.adyen.checkout.googlepay.internal.data.model.GooglePayPaymentMethodModel @@ -69,7 +69,7 @@ internal class DefaultGooglePayDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -102,7 +102,7 @@ internal class DefaultGooglePayDelegate( @VisibleForTesting internal fun updateComponentState(paymentData: PaymentData?) { - Logger.v(TAG, "updateComponentState") + adyenLog(AdyenLogLevel.VERBOSE) { "updateComponentState" } val componentState = createComponentState(paymentData) _componentStateFlow.tryEmit(componentState) } @@ -132,7 +132,7 @@ internal class DefaultGooglePayDelegate( } override fun startGooglePayScreen(activity: Activity, requestCode: Int) { - Logger.d(TAG, "startGooglePayScreen") + adyenLog(AdyenLogLevel.DEBUG) { "startGooglePayScreen" } val paymentsClient = Wallet.getPaymentsClient(activity, GooglePayUtils.createWalletOptions(componentParams)) val paymentDataRequest = GooglePayUtils.createPaymentDataRequest(componentParams) // TODO this forces us to use the deprecated onActivityResult. Look into alternatives when/if Google provides @@ -141,7 +141,7 @@ internal class DefaultGooglePayDelegate( } override fun handleActivityResult(resultCode: Int, data: Intent?) { - Logger.d(TAG, "handleActivityResult") + adyenLog(AdyenLogLevel.DEBUG) { "handleActivityResult" } when (resultCode) { Activity.RESULT_OK -> { if (data == null) { @@ -182,8 +182,4 @@ internal class DefaultGooglePayDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParams.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParams.kt index 4ce10ee29b..e6c7526487 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParams.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParams.kt @@ -9,20 +9,14 @@ package com.adyen.checkout.googlepay.internal.ui.model import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment import com.adyen.checkout.googlepay.BillingAddressParameters import com.adyen.checkout.googlepay.MerchantInfo import com.adyen.checkout.googlepay.ShippingAddressParameters -import java.util.Locale internal data class GooglePayComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, + private val commonComponentParams: CommonComponentParams, override val amount: Amount, val gatewayMerchantId: String, val googlePayEnvironment: Int, @@ -40,4 +34,4 @@ internal data class GooglePayComponentParams( val shippingAddressParameters: ShippingAddressParameters?, val isBillingAddressRequired: Boolean, val billingAddressParameters: BillingAddressParameters?, -) : ComponentParams +) : ComponentParams by commonComponentParams diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt index c97e413667..ee737a5192 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapper.kt @@ -9,62 +9,73 @@ package com.adyen.checkout.googlepay.internal.ui.model import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.CheckoutCurrency import com.adyen.checkout.components.core.PaymentMethod -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.googlepay.AllowedAuthMethods import com.adyen.checkout.googlepay.AllowedCardNetworks import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.googlepay.getGooglePayConfiguration import com.google.android.gms.wallet.WalletConstants +import java.util.Locale internal class GooglePayComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { fun mapToParams( - googlePayConfiguration: GooglePayConfiguration, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, paymentMethod: PaymentMethod, - sessionParams: SessionParams?, ): GooglePayComponentParams { - return googlePayConfiguration - .mapToParamsInternal(paymentMethod) - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, + ) + val googlePayConfiguration = checkoutConfiguration.getGooglePayConfiguration() + return mapToParams( + commonComponentParamsMapperData.commonComponentParams, + googlePayConfiguration, + paymentMethod, + ) } - private fun GooglePayConfiguration.mapToParamsInternal( + private fun mapToParams( + commonComponentParams: CommonComponentParams, + googlePayConfiguration: GooglePayConfiguration?, paymentMethod: PaymentMethod, ): GooglePayComponentParams { return GooglePayComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - gatewayMerchantId = getPreferredGatewayMerchantId(paymentMethod), - allowedAuthMethods = getAvailableAuthMethods(), - allowedCardNetworks = getAvailableCardNetworks(paymentMethod), - googlePayEnvironment = getGooglePayEnvironment(), - amount = amount ?: DEFAULT_AMOUNT, - totalPriceStatus = totalPriceStatus ?: DEFAULT_TOTAL_PRICE_STATUS, - countryCode = countryCode, - merchantInfo = merchantInfo, - isAllowPrepaidCards = isAllowPrepaidCards ?: false, - isAllowCreditCards = isAllowCreditCards, - isAssuranceDetailsRequired = isAssuranceDetailsRequired, - isEmailRequired = isEmailRequired ?: false, - isExistingPaymentMethodRequired = isExistingPaymentMethodRequired ?: false, - isShippingAddressRequired = isShippingAddressRequired ?: false, - shippingAddressParameters = shippingAddressParameters, - isBillingAddressRequired = isBillingAddressRequired ?: false, - billingAddressParameters = billingAddressParameters, + commonComponentParams = commonComponentParams, + amount = commonComponentParams.amount ?: DEFAULT_AMOUNT, + gatewayMerchantId = googlePayConfiguration.getPreferredGatewayMerchantId(paymentMethod), + allowedAuthMethods = googlePayConfiguration.getAvailableAuthMethods(), + allowedCardNetworks = googlePayConfiguration.getAvailableCardNetworks(paymentMethod), + googlePayEnvironment = getGooglePayEnvironment(commonComponentParams.environment, googlePayConfiguration), + totalPriceStatus = googlePayConfiguration?.totalPriceStatus ?: DEFAULT_TOTAL_PRICE_STATUS, + countryCode = googlePayConfiguration?.countryCode, + merchantInfo = googlePayConfiguration?.merchantInfo, + isAllowPrepaidCards = googlePayConfiguration?.isAllowPrepaidCards ?: false, + isAllowCreditCards = googlePayConfiguration?.isAllowCreditCards, + isAssuranceDetailsRequired = googlePayConfiguration?.isAssuranceDetailsRequired, + isEmailRequired = googlePayConfiguration?.isEmailRequired ?: false, + isExistingPaymentMethodRequired = googlePayConfiguration?.isExistingPaymentMethodRequired ?: false, + isShippingAddressRequired = googlePayConfiguration?.isShippingAddressRequired ?: false, + shippingAddressParameters = googlePayConfiguration?.shippingAddressParameters, + isBillingAddressRequired = googlePayConfiguration?.isBillingAddressRequired ?: false, + billingAddressParameters = googlePayConfiguration?.billingAddressParameters, ) } @@ -72,10 +83,10 @@ internal class GooglePayComponentParamsMapper( * Returns the [GooglePayConfiguration.merchantAccount] if set, or falls back to the * paymentMethod.configuration.gatewayMerchantId field returned by the API. */ - private fun GooglePayConfiguration.getPreferredGatewayMerchantId( + private fun GooglePayConfiguration?.getPreferredGatewayMerchantId( paymentMethod: PaymentMethod, ): String { - return merchantAccount + return this?.merchantAccount ?: paymentMethod.configuration?.gatewayMerchantId ?: throw ComponentException( "GooglePay merchantAccount not found. Update your API version or pass it manually inside your " + @@ -83,15 +94,15 @@ internal class GooglePayComponentParamsMapper( ) } - private fun GooglePayConfiguration.getAvailableAuthMethods(): List { - return allowedAuthMethods + private fun GooglePayConfiguration?.getAvailableAuthMethods(): List { + return this?.allowedAuthMethods ?: AllowedAuthMethods.allAllowedAuthMethods } - private fun GooglePayConfiguration.getAvailableCardNetworks( + private fun GooglePayConfiguration?.getAvailableCardNetworks( paymentMethod: PaymentMethod ): List { - return allowedCardNetworks + return this?.allowedCardNetworks ?: getAvailableCardNetworksFromApi(paymentMethod) ?: AllowedCardNetworks.allAllowedCardNetworks } @@ -100,7 +111,9 @@ internal class GooglePayComponentParamsMapper( val brands = paymentMethod.brands ?: return null return brands.mapNotNull { brand -> val network = mapBrandToGooglePayNetwork(brand) - if (network == null) Logger.e(TAG, "skipping brand $brand, as it is not an allowed card network.") + if (network == null) { + adyenLog(AdyenLogLevel.ERROR) { "skipping brand $brand, as it is not an allowed card network." } + } return@mapNotNull network } } @@ -113,38 +126,18 @@ internal class GooglePayComponentParamsMapper( } } - private fun GooglePayConfiguration.getGooglePayEnvironment(): Int { + private fun getGooglePayEnvironment( + environment: Environment, + googlePayConfiguration: GooglePayConfiguration? + ): Int { return when { - googlePayEnvironment != null -> googlePayEnvironment + googlePayConfiguration?.googlePayEnvironment != null -> googlePayConfiguration.googlePayEnvironment environment == Environment.TEST -> WalletConstants.ENVIRONMENT_TEST else -> WalletConstants.ENVIRONMENT_PRODUCTION } } - private fun GooglePayComponentParams.override(overrideComponentParams: ComponentParams?): GooglePayComponentParams { - if (overrideComponentParams == null) return this - val amount = overrideComponentParams.amount ?: DEFAULT_AMOUNT - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = amount, - ) - } - - private fun GooglePayComponentParams.override( - sessionParams: SessionParams? = null - ): GooglePayComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, - ) - } - companion object { - private val TAG = LogUtil.getTag() private val DEFAULT_AMOUNT = Amount(currency = CheckoutCurrency.USD.name, value = 0) private const val DEFAULT_TOTAL_PRICE_STATUS = "FINAL" } diff --git a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt index 7bb0b62994..13b03e4281 100644 --- a/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt +++ b/googlepay/src/main/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtils.kt @@ -9,9 +9,9 @@ package com.adyen.checkout.googlepay.internal.util import com.adyen.checkout.components.core.internal.util.AmountFormat import com.adyen.checkout.components.core.paymentmethod.GooglePayPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.googlepay.internal.data.model.CardParameters import com.adyen.checkout.googlepay.internal.data.model.GooglePayPaymentMethodModel import com.adyen.checkout.googlepay.internal.data.model.IsReadyToPayRequestModel @@ -34,7 +34,6 @@ import java.util.Locale @Suppress("TooManyFunctions") internal object GooglePayUtils { - private val TAG = LogUtil.getTag() private val GOOGLE_PAY_DECIMAL_FORMAT = DecimalFormat("0.##", DecimalFormatSymbols(Locale.ROOT)) private const val GOOGLE_PAY_DECIMAL_SCALE = 2 @@ -146,7 +145,7 @@ internal object GooglePayUtils { googlePayCardNetwork = infoJson.getString(CARD_NETWORK) } } catch (e: JSONException) { - Logger.e(TAG, "Failed to find Google Pay token.", e) + adyenLog(AdyenLogLevel.ERROR, e) { "Failed to find Google Pay token." } } } } diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayComponentTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayComponentTest.kt index 4d359e2a74..a90f3c9869 100644 --- a/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayComponentTest.kt +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayComponentTest.kt @@ -17,9 +17,8 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.googlepay.internal.ui.GooglePayDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -41,7 +40,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class GooglePayComponentTest( @Mock private val googlePayDelegate: GooglePayDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -61,7 +60,6 @@ internal class GooglePayComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -115,7 +113,7 @@ internal class GooglePayComponentTest( googlePayDelegate = googlePayDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) component.viewFlow.test { diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayConfigurationTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayConfigurationTest.kt new file mode 100644 index 0000000000..e75b35bf10 --- /dev/null +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/GooglePayConfigurationTest.kt @@ -0,0 +1,164 @@ +package com.adyen.checkout.googlepay + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.google.android.gms.wallet.WalletConstants +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class GooglePayConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + googlePay { + setMerchantAccount("merchantAccount") + setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) + setMerchantInfo(MerchantInfo(merchantId = "id")) + setCountryCode("US") + setAllowedAuthMethods(listOf(AllowedAuthMethods.PAN_ONLY)) + setAllowedCardNetworks(listOf(AllowedCardNetworks.VISA)) + setAllowPrepaidCards(true) + setAllowCreditCards(false) + setAssuranceDetailsRequired(true) + setEmailRequired(true) + setExistingPaymentMethodRequired(true) + setShippingAddressRequired(true) + setShippingAddressParameters(ShippingAddressParameters(isPhoneNumberRequired = true)) + setBillingAddressRequired(true) + setBillingAddressParameters(BillingAddressParameters(format = "format")) + setTotalPriceStatus("status") + } + } + + val actual = checkoutConfiguration.getGooglePayConfiguration() + + val expected = GooglePayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setMerchantAccount("merchantAccount") + .setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) + .setMerchantInfo(MerchantInfo(merchantId = "id")) + .setCountryCode("US") + .setAllowedAuthMethods(listOf(AllowedAuthMethods.PAN_ONLY)) + .setAllowedCardNetworks(listOf(AllowedCardNetworks.VISA)) + .setAllowPrepaidCards(true) + .setAllowCreditCards(false) + .setAssuranceDetailsRequired(true) + .setEmailRequired(true) + .setExistingPaymentMethodRequired(true) + .setShippingAddressRequired(true) + .setShippingAddressParameters(ShippingAddressParameters(isPhoneNumberRequired = true)) + .setBillingAddressRequired(true) + .setBillingAddressParameters(BillingAddressParameters(format = "format")) + .setTotalPriceStatus("status") + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.merchantAccount, actual?.merchantAccount) + assertEquals(expected.googlePayEnvironment, actual?.googlePayEnvironment) + assertEquals(expected.merchantInfo, actual?.merchantInfo) + assertEquals(expected.countryCode, actual?.countryCode) + assertEquals(expected.allowedAuthMethods, actual?.allowedAuthMethods) + assertEquals(expected.allowedCardNetworks, actual?.allowedCardNetworks) + assertEquals(expected.isAllowPrepaidCards, actual?.isAllowPrepaidCards) + assertEquals(expected.isAllowCreditCards, actual?.isAllowCreditCards) + assertEquals(expected.isAssuranceDetailsRequired, actual?.isAssuranceDetailsRequired) + assertEquals(expected.isEmailRequired, actual?.isEmailRequired) + assertEquals(expected.isExistingPaymentMethodRequired, actual?.isExistingPaymentMethodRequired) + assertEquals(expected.isShippingAddressRequired, actual?.isShippingAddressRequired) + assertEquals(expected.shippingAddressParameters, actual?.shippingAddressParameters) + assertEquals(expected.isBillingAddressRequired, actual?.isBillingAddressRequired) + assertEquals(expected.billingAddressParameters, actual?.billingAddressParameters) + assertEquals(expected.totalPriceStatus, actual?.totalPriceStatus) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = GooglePayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setMerchantAccount("merchantAccount") + .setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) + .setMerchantInfo(MerchantInfo(merchantId = "id")) + .setCountryCode("US") + .setAllowedAuthMethods(listOf(AllowedAuthMethods.PAN_ONLY)) + .setAllowedCardNetworks(listOf(AllowedCardNetworks.VISA)) + .setAllowPrepaidCards(true) + .setAllowCreditCards(false) + .setAssuranceDetailsRequired(true) + .setEmailRequired(true) + .setExistingPaymentMethodRequired(true) + .setShippingAddressRequired(true) + .setShippingAddressParameters(ShippingAddressParameters(isPhoneNumberRequired = true)) + .setBillingAddressRequired(true) + .setBillingAddressParameters(BillingAddressParameters(format = "format")) + .setTotalPriceStatus("status") + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualGooglePayCardConfig = actual.getGooglePayConfiguration() + assertEquals(config.shopperLocale, actualGooglePayCardConfig?.shopperLocale) + assertEquals(config.environment, actualGooglePayCardConfig?.environment) + assertEquals(config.clientKey, actualGooglePayCardConfig?.clientKey) + assertEquals(config.amount, actualGooglePayCardConfig?.amount) + assertEquals(config.analyticsConfiguration, actualGooglePayCardConfig?.analyticsConfiguration) + assertEquals(config.merchantAccount, actualGooglePayCardConfig?.merchantAccount) + assertEquals(config.googlePayEnvironment, actualGooglePayCardConfig?.googlePayEnvironment) + assertEquals(config.merchantInfo, actualGooglePayCardConfig?.merchantInfo) + assertEquals(config.countryCode, actualGooglePayCardConfig?.countryCode) + assertEquals(config.allowedAuthMethods, actualGooglePayCardConfig?.allowedAuthMethods) + assertEquals(config.allowedCardNetworks, actualGooglePayCardConfig?.allowedCardNetworks) + assertEquals(config.isAllowPrepaidCards, actualGooglePayCardConfig?.isAllowPrepaidCards) + assertEquals(config.isAllowCreditCards, actualGooglePayCardConfig?.isAllowCreditCards) + assertEquals(config.isAssuranceDetailsRequired, actualGooglePayCardConfig?.isAssuranceDetailsRequired) + assertEquals(config.isEmailRequired, actualGooglePayCardConfig?.isEmailRequired) + assertEquals(config.isExistingPaymentMethodRequired, actualGooglePayCardConfig?.isExistingPaymentMethodRequired) + assertEquals(config.isShippingAddressRequired, actualGooglePayCardConfig?.isShippingAddressRequired) + assertEquals(config.shippingAddressParameters, actualGooglePayCardConfig?.shippingAddressParameters) + assertEquals(config.isBillingAddressRequired, actualGooglePayCardConfig?.isBillingAddressRequired) + assertEquals(config.billingAddressParameters, actualGooglePayCardConfig?.billingAddressParameters) + assertEquals(config.totalPriceStatus, actualGooglePayCardConfig?.totalPriceStatus) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegateTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegateTest.kt index d716ba0424..dff2e9fdf9 100644 --- a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegateTest.kt +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/DefaultGooglePayDelegateTest.kt @@ -10,14 +10,17 @@ package com.adyen.checkout.googlepay.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Configuration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.paymentmethod.GooglePayPaymentMethod import com.adyen.checkout.core.Environment import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.googlepay.googlePay import com.adyen.checkout.googlepay.internal.ui.model.GooglePayComponentParamsMapper import com.google.android.gms.wallet.PaymentData import kotlinx.coroutines.CoroutineScope @@ -116,9 +119,7 @@ internal class DefaultGooglePayDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getGooglePayConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createGooglePayDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -151,29 +152,30 @@ internal class DefaultGooglePayDelegateTest( } } - private fun getGooglePayConfigurationBuilder(): GooglePayConfiguration.Builder { - return GooglePayConfiguration.Builder( - Locale.US, - Environment.TEST, - "test_qwertyuiopasdfghjklzxcvbnmqwerty" - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: GooglePayConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = "test_qwertyuiopasdfghjklzxcvbnmqwerty", + amount = amount, + ) { + googlePay(configuration) } private fun createGooglePayDelegate( - configuration: GooglePayConfiguration = getGooglePayConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), paymentMethod: PaymentMethod = PaymentMethod( - configuration = Configuration(gatewayMerchantId = "TEST_GATEWAY_MERCHANT_ID") + configuration = Configuration(gatewayMerchantId = "TEST_GATEWAY_MERCHANT_ID"), ), ): DefaultGooglePayDelegate { return DefaultGooglePayDelegate( observerRepository = PaymentObserverRepository(), paymentMethod = PaymentMethod(), order = TEST_ORDER, - componentParams = GooglePayComponentParamsMapper(null, null).mapToParams( - configuration, - paymentMethod, - null - ), + componentParams = GooglePayComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null, paymentMethod), analyticsRepository = analyticsRepository, ) } diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt index 0810627a61..25bf35d10c 100644 --- a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/ui/model/GooglePayComponentParamsMapperTest.kt @@ -9,45 +9,54 @@ package com.adyen.checkout.googlepay.internal.ui.model import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Configuration import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionParams -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.googlepay.AllowedAuthMethods import com.adyen.checkout.googlepay.AllowedCardNetworks import com.adyen.checkout.googlepay.BillingAddressParameters import com.adyen.checkout.googlepay.GooglePayConfiguration import com.adyen.checkout.googlepay.MerchantInfo import com.adyen.checkout.googlepay.ShippingAddressParameters +import com.adyen.checkout.googlepay.googlePay +import com.adyen.checkout.test.LoggingExtension import com.google.android.gms.wallet.WalletConstants import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments.arguments import org.junit.jupiter.params.provider.MethodSource import java.util.Locale +@ExtendWith(LoggingExtension::class) internal class GooglePayComponentParamsMapperTest { - @BeforeEach - fun beforeEach() { - AdyenLogger.setLogLevel(Logger.NONE) - } + private val googlePayComponentParamsMapper = GooglePayComponentParamsMapper(CommonComponentParamsMapper()) @Test - fun `when parent configuration is null and custom google pay configuration fields are null then all fields should match`() { - val googlePayConfiguration = getGooglePayConfigurationBuilder().build() - - val params = - GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) + fun `when drop-in override params are null and custom google pay configuration fields are null then all fields should match`() { + val configuration = createCheckoutConfiguration() + + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), + ) val expected = getGooglePayComponentParams() @@ -55,7 +64,7 @@ internal class GooglePayComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom google pay configuration fields are set then all fields should match`() { + fun `when drop-in override params are null and custom google pay configuration fields are set then all fields should match`() { val amount = Amount("EUR", 1337) val merchantInfo = MerchantInfo("MERCHANT_NAME", "MERCHANT_ID") val allowedAuthMethods = listOf("AUTH1", "AUTH2", "AUTH3") @@ -63,30 +72,39 @@ internal class GooglePayComponentParamsMapperTest { val shippingAddressParameters = ShippingAddressParameters(listOf("ZZ", "AA"), true) val billingAddressParameters = BillingAddressParameters("FORMAT", true) - val googlePayConfiguration = GooglePayConfiguration.Builder( + val configuration = CheckoutConfiguration( shopperLocale = Locale.FRANCE, environment = Environment.APSE, clientKey = TEST_CLIENT_KEY_2, - ).setAmount(amount).setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) - .setMerchantAccount("MERCHANT_ACCOUNT") - .setAllowPrepaidCards(true) - .setAllowCreditCards(true) - .setAssuranceDetailsRequired(true) - .setCountryCode("ZZ") - .setMerchantInfo(merchantInfo) - .setAllowedAuthMethods(allowedAuthMethods) - .setAllowedCardNetworks(allowedCardNetworks) - .setBillingAddressParameters(billingAddressParameters) - .setBillingAddressRequired(true) - .setEmailRequired(true) - .setExistingPaymentMethodRequired(true) - .setShippingAddressParameters(shippingAddressParameters) - .setShippingAddressRequired(true) - .setTotalPriceStatus("STATUS") - .build() - - val params = - GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) + amount = amount, + ) { + googlePay { + setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) + setMerchantAccount("MERCHANT_ACCOUNT") + setAllowPrepaidCards(true) + setAllowCreditCards(true) + setAssuranceDetailsRequired(true) + setCountryCode("ZZ") + setMerchantInfo(merchantInfo) + setAllowedAuthMethods(allowedAuthMethods) + setAllowedCardNetworks(allowedCardNetworks) + setBillingAddressParameters(billingAddressParameters) + setBillingAddressRequired(true) + setEmailRequired(true) + setExistingPaymentMethodRequired(true) + setShippingAddressParameters(shippingAddressParameters) + setShippingAddressRequired(true) + setTotalPriceStatus("STATUS") + } + } + + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), + ) val expected = getGooglePayComponentParams( shopperLocale = Locale.FRANCE, @@ -115,38 +133,43 @@ internal class GooglePayComponentParamsMapperTest { } @Test - fun `when parent configuration is set then parent configuration fields should override google pay configuration fields`() { - val googlePayConfiguration = getGooglePayConfigurationBuilder().build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override google pay configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "XCD", value = 4_00L, ), - ) + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + googlePay { + setAmount(Amount("USD", 1L)) + setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + setMerchantAccount(TEST_GATEWAY_MERCHANT_ID) + } + } - val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( - googlePayConfiguration, - PaymentMethod(), - null, + val dropInOverrideParams = DropInOverrideParams(Amount("CAD", 123L), null) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + paymentMethod = PaymentMethod(), ) val expected = getGooglePayComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, + googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), isCreatedByDropIn = true, amount = Amount( - currency = "XCD", - value = 4_00L, + currency = "CAD", + value = 123L, ), ) @@ -155,11 +178,9 @@ internal class GooglePayComponentParamsMapperTest { @Test fun `when merchantAccount is set in googlePayConfiguration then it takes priority over gatewayMerchantId in the paymentMethod configuration`() { - val googlePayConfiguration = GooglePayConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1, - ).setMerchantAccount("GATEWAY_MERCHANT_ID_1").build() + val configuration = createCheckoutConfiguration { + setMerchantAccount("GATEWAY_MERCHANT_ID_1") + } val paymentMethod = PaymentMethod( configuration = Configuration( @@ -167,7 +188,13 @@ internal class GooglePayComponentParamsMapperTest { ), ) - val params = GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, paymentMethod, null) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = paymentMethod, + ) val expected = getGooglePayComponentParams( gatewayMerchantId = "GATEWAY_MERCHANT_ID_1", @@ -178,11 +205,12 @@ internal class GooglePayComponentParamsMapperTest { @Test fun `when merchantAccount is not set in googlePayConfiguration then gatewayMerchantId in the paymentMethod configuration is used`() { - val googlePayConfiguration = GooglePayConfiguration.Builder( - shopperLocale = Locale.US, + val configuration = CheckoutConfiguration( environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, - ).build() + ) { + googlePay() + } val paymentMethod = PaymentMethod( configuration = Configuration( @@ -190,7 +218,13 @@ internal class GooglePayComponentParamsMapperTest { ), ) - val params = GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, paymentMethod, null) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = paymentMethod, + ) val expected = getGooglePayComponentParams( gatewayMerchantId = "GATEWAY_MERCHANT_ID_2", @@ -201,26 +235,39 @@ internal class GooglePayComponentParamsMapperTest { @Test fun `when neither merchantAccount in googlePayConfiguration nor gatewayMerchantId in the paymentMethod configuration is set then exception is thrown`() { - val googlePayConfiguration = GooglePayConfiguration.Builder( - shopperLocale = Locale.US, + val configuration = CheckoutConfiguration( environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, - ).build() + ) { + googlePay() + } assertThrows { - GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) + googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), + ) } } @Test fun `when allowedCardNetworks is not set in googlePayConfiguration then brands in the paymentMethod is used`() { - val googlePayConfiguration = getGooglePayConfigurationBuilder().build() + val configuration = createCheckoutConfiguration() val paymentMethod = PaymentMethod( brands = listOf("mc", "amex", "maestro", "discover"), ) - val params = GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, paymentMethod, null) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = paymentMethod, + ) val expected = getGooglePayComponentParams( allowedCardNetworks = listOf("MASTERCARD", "AMEX", "DISCOVER"), @@ -231,11 +278,17 @@ internal class GooglePayComponentParamsMapperTest { @Test fun `when google pay environment is explicitly set then its value shouldn't change`() { - val googlePayConfiguration = - getGooglePayConfigurationBuilder().setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION).build() + val configuration = createCheckoutConfiguration { + setGooglePayEnvironment(WalletConstants.ENVIRONMENT_PRODUCTION) + } - val params = - GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), + ) val expected = getGooglePayComponentParams( googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, @@ -246,10 +299,15 @@ internal class GooglePayComponentParamsMapperTest { @Test fun `when google pay environment is not set and environment is TEST then google pay environment should be ENVIRONMENT_TEST`() { - val googlePayConfiguration = getGooglePayConfigurationBuilder().build() - - val params = - GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) + val configuration = createCheckoutConfiguration() + + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), + ) val expected = getGooglePayComponentParams( googlePayEnvironment = WalletConstants.ENVIRONMENT_TEST, @@ -260,14 +318,23 @@ internal class GooglePayComponentParamsMapperTest { @Test fun `when google pay environment is not set and environment is a live one then google pay environment should be ENVIRONMENT_PRODUCTION`() { - val googlePayConfiguration = GooglePayConfiguration.Builder( + val configuration = CheckoutConfiguration( shopperLocale = Locale.CHINA, environment = Environment.UNITED_STATES, clientKey = TEST_CLIENT_KEY_2, - ).setMerchantAccount(TEST_GATEWAY_MERCHANT_ID).build() + ) { + googlePay { + setMerchantAccount(TEST_GATEWAY_MERCHANT_ID) + } + } - val params = - GooglePayComponentParamsMapper(null, null).mapToParams(googlePayConfiguration, PaymentMethod(), null) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), + ) val expected = getGooglePayComponentParams( shopperLocale = Locale.CHINA, @@ -280,28 +347,18 @@ internal class GooglePayComponentParamsMapperTest { } @Test - fun `when amount is not set in parent configuration and google pay configuration then params amount should have 0 USD DEFAULT_VALUE`() { - val googlePayConfiguration = getGooglePayConfigurationBuilder().build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false, - amount = null, - ) - - val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( - googlePayConfiguration, - PaymentMethod(), - null, + fun `when amount is not set in checkout configuration and google pay configuration then params amount should have 0 USD DEFAULT_VALUE`() { + val configuration = createCheckoutConfiguration(amount = null) + + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), ) val expected = getGooglePayComponentParams( - shopperLocale = Locale.US, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -312,37 +369,21 @@ internal class GooglePayComponentParamsMapperTest { } @Test - fun `when parent configuration is set with empty amount then params amount should have 0 USD DEFAULT_VALUE`() { - // Google Pay Config is set with an amount which will be overridden by parent configuration - val googlePayConfiguration = getGooglePayConfigurationBuilder() - .setAmount( - Amount( - currency = "TRY", - value = 40_00L, - ), - ) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - // parent configuration overrides amount to be Amount.EMPTY - val overrideParams = GenericComponentParams( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false, - amount = null, - ) + fun `when checkout configuration is used with null amount then params amount should have 0 USD DEFAULT_VALUE`() { + // Google Pay Config is set with an amount which will be overridden by checkout configuration params + val configuration = createCheckoutConfiguration(amount = null) { + setAmount(Amount(currency = "TRY", value = 40_00L)) + } - val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( - googlePayConfiguration, - PaymentMethod(), - null, + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + paymentMethod = PaymentMethod(), ) val expected = getGooglePayComponentParams( - shopperLocale = Locale.US, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -354,54 +395,136 @@ internal class GooglePayComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val googlePayConfiguration = getGooglePayConfigurationBuilder() - .setAmount(configurationValue) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getGooglePayComponentParams(amount = it) } - - val params = GooglePayComponentParamsMapper(overrideParams, null).mapToParams( - googlePayConfiguration, - PaymentMethod(), - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ), + val configuration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), ) val expected = getGooglePayComponentParams( amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) + + assertEquals(expected, params) + } + + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, + ) + + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), + ) + + val expected = getGooglePayComponentParams( + shopperLocale = expectedValue, ) assertEquals(expected, params) } - private fun getGooglePayConfigurationBuilder() = GooglePayConfiguration.Builder( - shopperLocale = Locale.US, + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = googlePayComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + paymentMethod = PaymentMethod(), + ) + + val expected = getGooglePayComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, + ) + + assertEquals(expected, params) + } + + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: GooglePayConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, - ).setMerchantAccount(TEST_GATEWAY_MERCHANT_ID) + amount = amount, + ) { + googlePay { + setMerchantAccount(TEST_GATEWAY_MERCHANT_ID) + apply(configuration) + } + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, + ) @Suppress("LongParameterList") private fun getGooglePayComponentParams( - shopperLocale: Locale = Locale.US, + shopperLocale: Locale = DEVICE_LOCALE, environment: Environment = Environment.TEST, clientKey: String = TEST_CLIENT_KEY_1, analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), isCreatedByDropIn: Boolean = false, gatewayMerchantId: String = TEST_GATEWAY_MERCHANT_ID, googlePayEnvironment: Int = WalletConstants.ENVIRONMENT_TEST, - amount: Amount = Amount("USD", 0), + amount: Amount? = null, totalPriceStatus: String = "FINAL", countryCode: String? = null, merchantInfo: MerchantInfo? = null, @@ -417,14 +540,17 @@ internal class GooglePayComponentParamsMapperTest { isBillingAddressRequired: Boolean = false, billingAddressParameters: BillingAddressParameters? = null, ) = GooglePayComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = analyticsParams, - isCreatedByDropIn = isCreatedByDropIn, + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), gatewayMerchantId = gatewayMerchantId, googlePayEnvironment = googlePayEnvironment, - amount = amount, + amount = amount ?: Amount("USD", 0), totalPriceStatus = totalPriceStatus, countryCode = countryCode, merchantInfo = merchantInfo, @@ -445,6 +571,7 @@ internal class GooglePayComponentParamsMapperTest { private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" private const val TEST_GATEWAY_MERCHANT_ID = "TEST_GATEWAY_MERCHANT_ID" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun amountSource() = listOf( @@ -453,5 +580,14 @@ internal class GooglePayComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt index 37e2f31e49..3e245d4110 100644 --- a/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt +++ b/googlepay/src/test/java/com/adyen/checkout/googlepay/internal/util/GooglePayUtilsTest.kt @@ -11,6 +11,7 @@ package com.adyen.checkout.googlepay.internal.util import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.core.Environment import com.adyen.checkout.googlepay.BillingAddressParameters import com.adyen.checkout.googlepay.MerchantInfo @@ -238,11 +239,14 @@ internal class GooglePayUtilsTest { private fun getEmptyGooglePayComponentParams(): GooglePayComponentParams { return GooglePayComponentParams( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = "CLIENT_KEY", - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false, + commonComponentParams = CommonComponentParams( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = "CLIENT_KEY", + analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + isCreatedByDropIn = false, + amount = null, + ), amount = Amount("USD", 0), gatewayMerchantId = "", googlePayEnvironment = WalletConstants.ENVIRONMENT_TEST, @@ -265,11 +269,14 @@ internal class GooglePayUtilsTest { private fun getCustomGooglePayComponentParams(): GooglePayComponentParams { return GooglePayComponentParams( - shopperLocale = Locale.GERMAN, - environment = Environment.EUROPE, - clientKey = "CLIENT_KEY_CUSTOM", - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, + commonComponentParams = CommonComponentParams( + shopperLocale = Locale.GERMAN, + environment = Environment.EUROPE, + clientKey = "CLIENT_KEY_CUSTOM", + analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), + isCreatedByDropIn = true, + amount = Amount("EUR", 13_37), + ), amount = Amount("EUR", 13_37), gatewayMerchantId = "GATEWAY_MERCHANT_ID", googlePayEnvironment = WalletConstants.ENVIRONMENT_PRODUCTION, diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index ad73ab43e2..135300d3c4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -89,6 +89,14 @@ + + + + + + + + @@ -121,6 +129,14 @@ + + + + + + + + @@ -187,6 +203,14 @@ + + + + + + + + @@ -243,6 +267,14 @@ + + + + + + + + @@ -288,6 +320,14 @@ + + + + + + + + @@ -576,6 +616,14 @@ + + + + + + + + @@ -5303,6 +5351,14 @@ + + + + + + + + @@ -5492,6 +5548,14 @@ + + + + + + + + @@ -5532,6 +5596,14 @@ + + + + + + + + @@ -5572,6 +5644,14 @@ + + + + + + + + @@ -5644,6 +5724,14 @@ + + + + + + + + @@ -5684,6 +5772,14 @@ + + + + + + + + @@ -5724,6 +5820,14 @@ + + + + + + + + @@ -5764,6 +5868,14 @@ + + + + + + + + @@ -5804,6 +5916,14 @@ + + + + + + + + @@ -5829,6 +5949,11 @@ + + + + + @@ -6638,6 +6763,14 @@ + + + + + + + + @@ -6662,6 +6795,14 @@ + + + + + + + + @@ -6686,6 +6827,14 @@ + + + + + + + + @@ -6710,6 +6859,14 @@ + + + + + + + + @@ -6734,6 +6891,14 @@ + + + + + + + + @@ -6758,6 +6923,14 @@ + + + + + + + + @@ -6782,6 +6955,14 @@ + + + + + + + + @@ -6806,6 +6987,14 @@ + + + + + + + + @@ -6830,6 +7019,14 @@ + + + + + + + + @@ -6854,6 +7051,14 @@ + + + + + + + + @@ -6878,6 +7083,14 @@ + + + + + + + + @@ -6902,6 +7115,14 @@ + + + + + + + + @@ -6926,6 +7147,14 @@ + + + + + + + + @@ -6950,6 +7179,14 @@ + + + + + + + + @@ -6974,6 +7211,14 @@ + + + + + + + + @@ -9697,6 +9942,14 @@ + + + + + + + + @@ -9729,6 +9982,14 @@ + + + + + + + + @@ -9761,6 +10022,14 @@ + + + + + + + + @@ -9794,6 +10063,14 @@ + + + + + + + + @@ -9810,6 +10087,14 @@ + + + + + + + + @@ -9826,6 +10111,14 @@ + + + + + + + + @@ -9882,6 +10175,14 @@ + + + + + + + + @@ -9914,6 +10215,14 @@ + + + + + + + + @@ -9946,6 +10255,14 @@ + + + + + + + + @@ -10002,6 +10319,14 @@ + + + + + + + + @@ -10034,6 +10359,14 @@ + + + + + + + + @@ -10066,6 +10399,14 @@ + + + + + + + + @@ -10107,6 +10448,14 @@ + + + + + + + + @@ -10139,6 +10488,14 @@ + + + + + + + + @@ -10171,6 +10528,14 @@ + + + + + + + + @@ -10203,6 +10568,14 @@ + + + + + + + + @@ -10235,6 +10608,14 @@ + + + + + + + + @@ -10267,6 +10648,14 @@ + + + + + + + + @@ -10299,6 +10688,14 @@ + + + + + + + + @@ -10331,6 +10728,14 @@ + + + + + + + + @@ -10363,6 +10768,14 @@ + + + + + + + + @@ -10395,6 +10808,14 @@ + + + + + + + + @@ -10427,6 +10848,14 @@ + + + + + + + + @@ -10587,6 +11016,14 @@ + + + + + + + + @@ -10619,6 +11056,14 @@ + + + + + + + + @@ -10651,6 +11096,14 @@ + + + + + + + + @@ -10683,6 +11136,14 @@ + + + + + + + + @@ -10715,6 +11176,14 @@ + + + + + + + + @@ -10840,6 +11309,17 @@ + + + + + + + + + + + @@ -10956,6 +11436,11 @@ + + + + + @@ -11201,6 +11686,14 @@ + + + + + + + + @@ -11233,6 +11726,14 @@ + + + + + + + + @@ -11265,6 +11766,14 @@ + + + + + + + + @@ -11285,6 +11794,11 @@ + + + + + diff --git a/ideal/src/main/java/com/adyen/checkout/ideal/IdealConfiguration.kt b/ideal/src/main/java/com/adyen/checkout/ideal/IdealConfiguration.kt index c1c4255242..7689faa937 100644 --- a/ideal/src/main/java/com/adyen/checkout/ideal/IdealConfiguration.kt +++ b/ideal/src/main/java/com/adyen/checkout/ideal/IdealConfiguration.kt @@ -11,6 +11,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -23,7 +26,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class IdealConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -39,6 +42,22 @@ class IdealConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -46,14 +65,15 @@ class IdealConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -80,3 +100,38 @@ class IdealConfiguration private constructor( } } } + +fun CheckoutConfiguration.ideal( + configuration: @CheckoutConfigurationMarker IdealConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = IdealConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.IDEAL, config) + return this +} + +fun CheckoutConfiguration.getIdealConfiguration(): IdealConfiguration? { + return getConfiguration(PaymentMethodTypes.IDEAL) +} + +internal fun IdealConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.IDEAL, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/ideal/src/main/java/com/adyen/checkout/ideal/internal/provider/IdealComponentProvider.kt b/ideal/src/main/java/com/adyen/checkout/ideal/internal/provider/IdealComponentProvider.kt index d643098524..2c88bd6328 100644 --- a/ideal/src/main/java/com/adyen/checkout/ideal/internal/provider/IdealComponentProvider.kt +++ b/ideal/src/main/java/com/adyen/checkout/ideal/internal/provider/IdealComponentProvider.kt @@ -11,28 +11,28 @@ package com.adyen.checkout.ideal.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.IdealPaymentMethod import com.adyen.checkout.ideal.IdealComponent import com.adyen.checkout.ideal.IdealComponentState import com.adyen.checkout.ideal.IdealConfiguration +import com.adyen.checkout.ideal.getIdealConfiguration +import com.adyen.checkout.ideal.toCheckoutConfiguration import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate class IdealComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider( componentClass = IdealComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -57,4 +57,12 @@ constructor( override fun createPaymentMethod() = IdealPaymentMethod() override fun getSupportedPaymentMethods(): List = IdealComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): IdealConfiguration? { + return checkoutConfiguration.getIdealConfiguration() + } + + override fun getCheckoutConfiguration(configuration: IdealConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/ideal/src/test/java/com/adyen/checkout/ideal/IdealConfigurationTest.kt b/ideal/src/test/java/com/adyen/checkout/ideal/IdealConfigurationTest.kt new file mode 100644 index 0000000000..b1df9d09bd --- /dev/null +++ b/ideal/src/test/java/com/adyen/checkout/ideal/IdealConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.ideal + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class IdealConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + ideal { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getIdealConfiguration() + + val expected = IdealConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = IdealConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualIdealConfig = actual.getIdealConfiguration() + assertEquals(config.shopperLocale, actualIdealConfig?.shopperLocale) + assertEquals(config.environment, actualIdealConfig?.environment) + assertEquals(config.clientKey, actualIdealConfig?.clientKey) + assertEquals(config.amount, actualIdealConfig?.amount) + assertEquals(config.analyticsConfiguration, actualIdealConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualIdealConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualIdealConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualIdealConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentComponent.kt b/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentComponent.kt index c5e8e88106..f64efadebd 100644 --- a/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentComponent.kt +++ b/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentComponent.kt @@ -11,8 +11,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.instant.internal.provider.InstantPaymentComponentProvider import com.adyen.checkout.instant.internal.ui.InstantPaymentDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -64,14 +64,13 @@ class InstantPaymentComponent internal constructor( override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } instantPaymentDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = InstantPaymentComponentProvider() diff --git a/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentConfiguration.kt b/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentConfiguration.kt index fda228e79d..00e33f184f 100644 --- a/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentConfiguration.kt +++ b/instant/src/main/java/com/adyen/checkout/instant/InstantPaymentConfiguration.kt @@ -13,7 +13,9 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -23,7 +25,7 @@ import java.util.Locale */ @Parcelize class InstantPaymentConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -36,6 +38,22 @@ class InstantPaymentConfiguration private constructor( */ class Builder : ActionHandlingPaymentMethodConfigurationBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -43,14 +61,15 @@ class InstantPaymentConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -59,7 +78,7 @@ class InstantPaymentConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) override fun buildInternal(): InstantPaymentConfiguration { @@ -74,3 +93,43 @@ class InstantPaymentConfiguration private constructor( } } } + +private const val GLOBAL_INSTANT_CONFIG_KEY = "GLOBAL_INSTANT_CONFIG_KEY" + +fun CheckoutConfiguration.instantPayment( + paymentMethod: String = GLOBAL_INSTANT_CONFIG_KEY, + configuration: @CheckoutConfigurationMarker InstantPaymentConfiguration.Builder.() -> Unit = {}, +): CheckoutConfiguration { + val config = InstantPaymentConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(paymentMethod, config) + return this +} + +fun CheckoutConfiguration.getInstantPaymentConfiguration( + paymentMethod: String = GLOBAL_INSTANT_CONFIG_KEY, +): InstantPaymentConfiguration? { + return getConfiguration(paymentMethod) +} + +internal fun InstantPaymentConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(GLOBAL_INSTANT_CONFIG_KEY, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/instant/src/main/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProvider.kt b/instant/src/main/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProvider.kt index e402b0bcff..6928b8b3ad 100644 --- a/instant/src/main/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProvider.kt +++ b/instant/src/main/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProvider.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -28,17 +29,19 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryD import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.instant.InstantComponentState import com.adyen.checkout.instant.InstantPaymentComponent import com.adyen.checkout.instant.InstantPaymentConfiguration import com.adyen.checkout.instant.internal.ui.DefaultInstantPaymentDelegate +import com.adyen.checkout.instant.toCheckoutConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -52,31 +55,29 @@ import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory class InstantPaymentComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< InstantPaymentComponent, InstantPaymentConfiguration, InstantComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< InstantPaymentComponent, InstantPaymentConfiguration, InstantComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = GenericComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: InstantPaymentConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -85,7 +86,12 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -94,7 +100,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -107,8 +113,8 @@ constructor( analyticsRepository = analyticsRepository, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -129,6 +135,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: InstantPaymentConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): InstantPaymentComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -136,7 +166,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: InstantPaymentConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -144,10 +174,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -158,7 +191,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -171,8 +204,8 @@ constructor( analyticsRepository = analyticsRepository, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -187,7 +220,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -210,6 +243,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: InstantPaymentConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): InstantPaymentComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") @@ -218,6 +275,7 @@ constructor( override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean { return when { + PaymentMethodTypes.UNSUPPORTED_PAYMENT_METHODS.contains(paymentMethod.type) -> false PaymentMethodTypes.SUPPORTED_ACTION_ONLY_PAYMENT_METHODS.contains(paymentMethod.type) -> true PaymentMethodTypes.SUPPORTED_PAYMENT_METHODS.contains(paymentMethod.type) -> false else -> true diff --git a/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt b/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt index d79e742685..c623dafb3d 100644 --- a/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt +++ b/instant/src/main/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegate.kt @@ -20,8 +20,8 @@ import com.adyen.checkout.components.core.internal.ui.model.GenericComponentPara import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.GenericPaymentMethod import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.instant.InstantComponentState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel @@ -67,7 +67,7 @@ internal class DefaultInstantPaymentDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -84,7 +84,7 @@ internal class DefaultInstantPaymentDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -95,8 +95,4 @@ internal class DefaultInstantPaymentDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentComponentTest.kt b/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentComponentTest.kt index d68bed0e9e..60f95fa4f7 100644 --- a/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentComponentTest.kt +++ b/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentComponentTest.kt @@ -15,9 +15,8 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.instant.internal.ui.InstantPaymentDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -39,7 +38,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class InstantPaymentComponentTest( @Mock private val instantPaymentDelegate: InstantPaymentDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -59,7 +58,6 @@ internal class InstantPaymentComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -113,7 +111,7 @@ internal class InstantPaymentComponentTest( instantPaymentDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) component.viewFlow.test { diff --git a/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentConfigurationTest.kt b/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentConfigurationTest.kt new file mode 100644 index 0000000000..787b1ad1ee --- /dev/null +++ b/instant/src/test/java/com/adyen/checkout/instant/InstantPaymentConfigurationTest.kt @@ -0,0 +1,82 @@ +package com.adyen.checkout.instant + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class InstantPaymentConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + instantPayment("paypal") + } + + val actual = checkoutConfiguration.getInstantPaymentConfiguration("paypal") + + val expected = InstantPaymentConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = InstantPaymentConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualInstantConfig = actual.getInstantPaymentConfiguration() + assertEquals(config.shopperLocale, actualInstantConfig?.shopperLocale) + assertEquals(config.environment, actualInstantConfig?.environment) + assertEquals(config.clientKey, actualInstantConfig?.clientKey) + assertEquals(config.amount, actualInstantConfig?.amount) + assertEquals(config.analyticsConfiguration, actualInstantConfig?.analyticsConfiguration) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/instant/src/test/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProviderTest.kt b/instant/src/test/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProviderTest.kt new file mode 100644 index 0000000000..9c48f8eeff --- /dev/null +++ b/instant/src/test/java/com/adyen/checkout/instant/internal/provider/InstantPaymentComponentProviderTest.kt @@ -0,0 +1,49 @@ +package com.adyen.checkout.instant.internal.provider + +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodTypes +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource + +internal class InstantPaymentComponentProviderTest { + + private lateinit var provider: InstantPaymentComponentProvider + + @BeforeEach + fun setup() { + provider = InstantPaymentComponentProvider() + } + + @ParameterizedTest + @MethodSource("supportedSource") + fun `when payment method is, then should it be supported`(paymentMethodType: String, isSupported: Boolean) { + val paymentMethod = PaymentMethod(type = paymentMethodType) + + val result = provider.isPaymentMethodSupported(paymentMethod) + + assertEquals(isSupported, result) + } + + companion object { + + @JvmStatic + fun supportedSource() = + // Supported instant payment methods + listOf( + arguments("paypal", true), + arguments("klarna", true), + ) + + // Only action only payment methods are supported + PaymentMethodTypes.SUPPORTED_PAYMENT_METHODS.map { + val isSupported = PaymentMethodTypes.SUPPORTED_ACTION_ONLY_PAYMENT_METHODS.contains(it) + arguments(it, isSupported) + } + + // Unsupported payment methods are not supported + PaymentMethodTypes.UNSUPPORTED_PAYMENT_METHODS.map { + arguments(it, false) + } + } +} diff --git a/instant/src/test/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegateTest.kt b/instant/src/test/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegateTest.kt index d4b02df721..d16c1bfca4 100644 --- a/instant/src/test/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegateTest.kt +++ b/instant/src/test/java/com/adyen/checkout/instant/internal/ui/DefaultInstantPaymentDelegateTest.kt @@ -10,15 +10,15 @@ package com.adyen.checkout.instant.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.instant.InstantPaymentConfiguration +import com.adyen.checkout.test.LoggingExtension import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -40,7 +40,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) class DefaultInstantPaymentDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, ) { @@ -50,7 +50,6 @@ class DefaultInstantPaymentDelegateTest( @BeforeEach fun before() { delegate = createInstantPaymentDelegate() - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -74,9 +73,7 @@ class DefaultInstantPaymentDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getInstantPaymentConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createInstantPaymentDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -106,23 +103,25 @@ class DefaultInstantPaymentDelegateTest( } } - private fun getInstantPaymentConfigurationBuilder(): InstantPaymentConfiguration.Builder { - return InstantPaymentConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) - } + private fun createCheckoutConfiguration( + amount: Amount? = null, + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) private fun createInstantPaymentDelegate( - configuration: InstantPaymentConfiguration = getInstantPaymentConfigurationBuilder().build() + configuration: CheckoutConfiguration = createCheckoutConfiguration(), ): DefaultInstantPaymentDelegate { return DefaultInstantPaymentDelegate( observerRepository = PaymentObserverRepository(), paymentMethod = PaymentMethod(type = TYPE), order = TEST_ORDER, - componentParams = GenericComponentParamsMapper(null, null).mapToParams(configuration, null), - analyticsRepository = analyticsRepository + componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), + analyticsRepository = analyticsRepository, ) } diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListComponent.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListComponent.kt index 48258b5620..7ef408033e 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListComponent.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListComponent.kt @@ -21,8 +21,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.paymentmethod.IssuerListPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -77,23 +77,20 @@ abstract class IssuerListComponent< override fun isConfirmationRequired(): Boolean = issuerListDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? IssuerListDelegate<*, *>)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } issuerListDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListConfiguration.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListConfiguration.kt index 21c2c34dcf..d139725671 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListConfiguration.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/IssuerListConfiguration.kt @@ -37,10 +37,16 @@ abstract class IssuerListConfiguration : Configuration, ButtonConfiguration { protected open var hideIssuerLogos: Boolean? = null protected open var isSubmitButtonVisible: Boolean? = null + protected constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + + @Deprecated("You can omit the context parameter") protected constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) protected constructor( diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt index 79cc79041f..29eeae3fad 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/provider/IssuerListComponentProvider.kt @@ -11,12 +11,14 @@ package com.adyen.checkout.issuerlist.internal.provider import android.app.Application import androidx.annotation.RestrictTo import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentComponentData @@ -31,17 +33,19 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryD import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.components.core.paymentmethod.IssuerListPaymentMethod import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.issuerlist.internal.IssuerListComponent import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration import com.adyen.checkout.issuerlist.internal.ui.DefaultIssuerListDelegate import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate +import com.adyen.checkout.issuerlist.internal.ui.model.IssuerListComponentParams import com.adyen.checkout.issuerlist.internal.ui.model.IssuerListComponentParamsMapper import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback @@ -54,7 +58,7 @@ import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponen import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler -@Suppress("ktlint:standard:type-parameter-list-spacing") +@Suppress("TooManyFunctions", "ktlint:standard:type-parameter-list-spacing") abstract class IssuerListComponentProvider< ComponentT : IssuerListComponent, ConfigurationT : IssuerListConfiguration, @@ -64,31 +68,25 @@ abstract class IssuerListComponentProvider< @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( private val componentClass: Class, - overrideComponentParams: ComponentParams?, - overrideSessionParams: SessionParams?, + private val dropInOverrideParams: DropInOverrideParams?, private val analyticsRepository: AnalyticsRepository?, - hideIssuerLogosDefaultValue: Boolean = false, + private val hideIssuerLogosDefaultValue: Boolean = false, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider>, SessionPaymentComponentProvider< ComponentT, ConfigurationT, ComponentStateT, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = IssuerListComponentParamsMapper( - hideIssuerLogosDefaultValue = hideIssuerLogosDefaultValue, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, - ) - final override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: ConfigurationT, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -97,7 +95,14 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = IssuerListComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + hideIssuerLogosDefaultValue = hideIssuerLogosDefaultValue, + componentConfiguration = getConfiguration(checkoutConfiguration), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -106,23 +111,20 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) - val issuerListDelegate = DefaultIssuerListDelegate( - observerRepository = PaymentObserverRepository(), + val issuerListDelegate = createDefaultDelegate( componentParams = componentParams, paymentMethod = paymentMethod, order = order, + savedStateHandle = savedStateHandle, analyticsRepository = analyticsRepository, - submitHandler = SubmitHandler(savedStateHandle), - typedPaymentMethodFactory = ::createPaymentMethod, - componentStateFactory = ::createComponentState ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -143,6 +145,30 @@ constructor( } } + final override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: ConfigurationT, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): ComponentT { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = getCheckoutConfiguration(configuration), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") final override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -150,7 +176,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: ConfigurationT, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -158,10 +184,15 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - issuerListConfiguration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = IssuerListComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + hideIssuerLogosDefaultValue = hideIssuerLogosDefaultValue, + componentConfiguration = getConfiguration(checkoutConfiguration), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -172,23 +203,20 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) - val issuerListDelegate = DefaultIssuerListDelegate( - observerRepository = PaymentObserverRepository(), + val issuerListDelegate = createDefaultDelegate( componentParams = componentParams, paymentMethod = paymentMethod, order = checkoutSession.order, + savedStateHandle = savedStateHandle, analyticsRepository = analyticsRepository, - submitHandler = SubmitHandler(savedStateHandle), - typedPaymentMethodFactory = ::createPaymentMethod, - componentStateFactory = ::createComponentState ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -203,7 +231,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -226,6 +254,50 @@ constructor( } } + @Suppress("LongMethod") + final override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: ConfigurationT, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): ComponentT { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = getCheckoutConfiguration(configuration), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + + private fun createDefaultDelegate( + componentParams: IssuerListComponentParams, + paymentMethod: PaymentMethod, + order: Order?, + savedStateHandle: SavedStateHandle, + analyticsRepository: AnalyticsRepository, + ): DefaultIssuerListDelegate { + return DefaultIssuerListDelegate( + observerRepository = PaymentObserverRepository(), + componentParams = componentParams, + paymentMethod = paymentMethod, + order = order, + analyticsRepository = analyticsRepository, + submitHandler = SubmitHandler(savedStateHandle), + typedPaymentMethodFactory = ::createPaymentMethod, + componentStateFactory = ::createComponentState, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") @@ -249,6 +321,10 @@ constructor( protected abstract fun getSupportedPaymentMethods(): List + protected abstract fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): ConfigurationT? + + protected abstract fun getCheckoutConfiguration(configuration: ConfigurationT): CheckoutConfiguration + final override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean { return getSupportedPaymentMethods().contains(paymentMethod.type) } diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegate.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegate.kt index 1861cdd2a0..d4ff7685ea 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegate.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegate.kt @@ -19,8 +19,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.paymentmethod.IssuerListPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.ui.model.IssuerListComponentParams import com.adyen.checkout.issuerlist.internal.ui.model.IssuerListInputData @@ -78,7 +78,7 @@ internal class DefaultIssuerListDelegate< } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -95,7 +95,7 @@ internal class DefaultIssuerListDelegate< submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -112,7 +112,7 @@ internal class DefaultIssuerListDelegate< override fun getIssuers(): List = paymentMethod.issuers?.mapToModel(componentParams.environment) ?: paymentMethod.details.getLegacyIssuers( - componentParams.environment + componentParams.environment, ) override fun updateInputData(update: IssuerListInputData.() -> Unit) { @@ -174,8 +174,4 @@ internal class DefaultIssuerListDelegate< override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParams.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParams.kt index 2aff4e5281..33dd9edabd 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParams.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParams.kt @@ -9,23 +9,15 @@ package com.adyen.checkout.issuerlist.internal.ui.model import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.ButtonParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType -import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class IssuerListComponentParams( - override val shopperLocale: Locale, - override val environment: Environment, - override val clientKey: String, - override val analyticsParams: AnalyticsParams, - override val isCreatedByDropIn: Boolean, - override val amount: Amount?, + private val commonComponentParams: CommonComponentParams, override val isSubmitButtonVisible: Boolean, val viewType: IssuerListViewType, val hideIssuerLogos: Boolean, -) : ComponentParams, ButtonParams +) : ComponentParams by commonComponentParams, ButtonParams diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapper.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapper.kt index b5f0e4c7c0..88e69846ae 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapper.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapper.kt @@ -9,63 +9,39 @@ package com.adyen.checkout.issuerlist.internal.ui.model import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration +import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class IssuerListComponentParamsMapper( - private val overrideComponentParams: ComponentParams?, - private val overrideSessionParams: SessionParams?, - private val hideIssuerLogosDefaultValue: Boolean = false, + private val commonComponentParamsMapper: CommonComponentParamsMapper, ) { + @Suppress("LongParameterList") fun mapToParams( - issuerListConfiguration: IssuerListConfiguration, - sessionParams: SessionParams?, + checkoutConfiguration: CheckoutConfiguration, + deviceLocale: Locale, + dropInOverrideParams: DropInOverrideParams?, + componentSessionParams: SessionParams?, + componentConfiguration: IssuerListConfiguration?, + hideIssuerLogosDefaultValue: Boolean, ): IssuerListComponentParams { - return issuerListConfiguration - .mapToParamsInternal() - .override(overrideComponentParams) - .override(sessionParams ?: overrideSessionParams) - } - - private fun IssuerListConfiguration.mapToParamsInternal(): IssuerListComponentParams { - return IssuerListComponentParams( - shopperLocale = shopperLocale, - environment = environment, - clientKey = clientKey, - analyticsParams = AnalyticsParams(analyticsConfiguration), - isCreatedByDropIn = false, - amount = amount, - isSubmitButtonVisible = isSubmitButtonVisible ?: true, - viewType = viewType ?: IssuerListViewType.RECYCLER_VIEW, - hideIssuerLogos = hideIssuerLogos ?: hideIssuerLogosDefaultValue, + val commonComponentParamsMapperData = commonComponentParamsMapper.mapToParams( + checkoutConfiguration, + deviceLocale, + dropInOverrideParams, + componentSessionParams, ) - } - - private fun IssuerListComponentParams.override( - overrideComponentParams: ComponentParams? - ): IssuerListComponentParams { - if (overrideComponentParams == null) return this - return copy( - shopperLocale = overrideComponentParams.shopperLocale, - environment = overrideComponentParams.environment, - clientKey = overrideComponentParams.clientKey, - analyticsParams = overrideComponentParams.analyticsParams, - isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn, - amount = overrideComponentParams.amount, - ) - } - - private fun IssuerListComponentParams.override( - sessionParams: SessionParams? = null - ): IssuerListComponentParams { - if (sessionParams == null) return this - return copy( - amount = sessionParams.amount ?: amount, + return IssuerListComponentParams( + commonComponentParams = commonComponentParamsMapperData.commonComponentParams, + isSubmitButtonVisible = componentConfiguration?.isSubmitButtonVisible ?: true, + viewType = componentConfiguration?.viewType ?: IssuerListViewType.RECYCLER_VIEW, + hideIssuerLogos = componentConfiguration?.hideIssuerLogos ?: hideIssuerLogosDefaultValue, ) } } diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListRecyclerView.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListRecyclerView.kt index 63e991570f..44a5786a2b 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListRecyclerView.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListRecyclerView.kt @@ -13,8 +13,8 @@ import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.issuerlist.databinding.IssuerListRecyclerViewBinding import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.issuerlist.internal.ui.model.IssuerModel @@ -29,7 +29,7 @@ internal class IssuerListRecyclerView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -71,14 +71,10 @@ internal class IssuerListRecyclerView @JvmOverloads constructor( } private fun onItemClicked(issuerModel: IssuerModel) { - Logger.d(TAG, "onItemClicked - ${issuerModel.name}") + adyenLog(AdyenLogLevel.DEBUG) { "onItemClicked - ${issuerModel.name}" } issuerListDelegate.updateInputData { selectedIssuer = issuerModel } issuerListDelegate.onSubmit() } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListSpinnerView.kt b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListSpinnerView.kt index f78699a4e3..483d74b431 100644 --- a/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListSpinnerView.kt +++ b/issuer-list/src/main/java/com/adyen/checkout/issuerlist/internal/ui/view/IssuerListSpinnerView.kt @@ -14,8 +14,8 @@ import android.view.View import android.widget.AdapterView import android.widget.LinearLayout import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.issuerlist.databinding.IssuerListSpinnerViewBinding import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentView @@ -29,7 +29,7 @@ internal class IssuerListSpinnerView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView, AdapterView.OnItemSelectedListener { @@ -72,8 +72,9 @@ internal class IssuerListSpinnerView @JvmOverloads constructor( } override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { - Logger.d(TAG, "onItemSelected - " + issuersAdapter.getItem(position).name) - issuerListDelegate.updateInputData { selectedIssuer = issuersAdapter.getItem(position) } + val item = issuersAdapter.getItem(position) + adyenLog(AdyenLogLevel.DEBUG) { "onItemSelected - " + item.name } + issuerListDelegate.updateInputData { selectedIssuer = item } } override fun setEnabled(enabled: Boolean) { @@ -86,8 +87,4 @@ internal class IssuerListSpinnerView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/IssuerListComponentTest.kt b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/IssuerListComponentTest.kt index ec9108f1b4..4671292520 100644 --- a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/IssuerListComponentTest.kt +++ b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/IssuerListComponentTest.kt @@ -15,17 +15,15 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.issuerlist.TestIssuerComponentState import com.adyen.checkout.issuerlist.internal.ui.IssuerListComponentViewType import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.issuerlist.utils.TestIssuerListComponent import com.adyen.checkout.issuerlist.utils.TestIssuerPaymentMethod +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -42,8 +40,7 @@ import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class IssuerListComponentTest( @Mock private val issuerListDelegate: IssuerListDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -66,7 +63,6 @@ internal class IssuerListComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegateTest.kt b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegateTest.kt index f27bf9b5e5..b9d877d93b 100644 --- a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegateTest.kt +++ b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/DefaultIssuerListDelegateTest.kt @@ -10,21 +10,21 @@ package com.adyen.checkout.issuerlist.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.core.AdyenLogger +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.TestIssuerComponentState -import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration import com.adyen.checkout.issuerlist.internal.ui.model.IssuerListComponentParamsMapper import com.adyen.checkout.issuerlist.internal.ui.model.IssuerListOutputData import com.adyen.checkout.issuerlist.internal.ui.model.IssuerModel import com.adyen.checkout.issuerlist.utils.TestIssuerListConfiguration import com.adyen.checkout.issuerlist.utils.TestIssuerPaymentMethod +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,7 +50,7 @@ import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultIssuerListDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, @Mock private val submitHandler: SubmitHandler, @@ -61,7 +61,6 @@ internal class DefaultIssuerListDelegateTest( @BeforeEach fun beforeEach() { delegate = createIssuerListDelegate() - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -130,9 +129,9 @@ internal class DefaultIssuerListDelegateTest( IssuerModel( id = "issuer-id", name = "issuer-name", - environment = Environment.TEST - ) - ) + environment = Environment.TEST, + ), + ), ) with(expectMostRecentItem()) { assertEquals("issuer-id", data.paymentMethod?.issuer) @@ -150,9 +149,7 @@ internal class DefaultIssuerListDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultTestIssuerListConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createIssuerListDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -167,17 +164,20 @@ internal class DefaultIssuerListDelegateTest( @Test fun `when configuration viewType is RECYCLER_VIEW then viewFlow should emit RECYCLER_VIEW`() = runTest { - val configuration: IssuerListConfiguration = TestIssuerListConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 - ) - .setViewType(IssuerListViewType.RECYCLER_VIEW) - .build() + val configuration = createCheckoutConfiguration { + setViewType(IssuerListViewType.RECYCLER_VIEW) + } delegate = DefaultIssuerListDelegate( observerRepository = PaymentObserverRepository(), - componentParams = IssuerListComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = IssuerListComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ), paymentMethod = PaymentMethod(), order = TEST_ORDER, analyticsRepository = analyticsRepository, @@ -187,9 +187,9 @@ internal class DefaultIssuerListDelegateTest( TestIssuerComponentState( data = data, isInputValid = isInputValid, - isReady = isReady + isReady = isReady, ) - } + }, ) delegate.viewFlow.test { @@ -199,17 +199,20 @@ internal class DefaultIssuerListDelegateTest( @Test fun `when configuration viewType is SPINNER_VIEW then viewFlow should emit SPINNER_VIEW`() = runTest { - val configuration: IssuerListConfiguration = TestIssuerListConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 - ) - .setViewType(IssuerListViewType.SPINNER_VIEW) - .build() + val configuration = createCheckoutConfiguration { + setViewType(IssuerListViewType.SPINNER_VIEW) + } delegate = DefaultIssuerListDelegate( observerRepository = PaymentObserverRepository(), - componentParams = IssuerListComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = IssuerListComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ), paymentMethod = PaymentMethod(), order = TEST_ORDER, analyticsRepository = analyticsRepository, @@ -219,9 +222,9 @@ internal class DefaultIssuerListDelegateTest( TestIssuerComponentState( data = data, isInputValid = isInputValid, - isReady = isReady + isReady = isReady, ) - } + }, ) delegate.viewFlow.test { assertEquals(IssuerListComponentViewType.SpinnerView, expectMostRecentItem()) @@ -240,10 +243,10 @@ internal class DefaultIssuerListDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createIssuerListDelegate( - configuration = getDefaultTestIssuerListConfigurationBuilder() - .setViewType(IssuerListViewType.SPINNER_VIEW) - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setViewType(IssuerListViewType.SPINNER_VIEW) + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -252,10 +255,10 @@ internal class DefaultIssuerListDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createIssuerListDelegate( - configuration = getDefaultTestIssuerListConfigurationBuilder() - .setViewType(IssuerListViewType.SPINNER_VIEW) - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setViewType(IssuerListViewType.SPINNER_VIEW) + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -309,25 +312,48 @@ internal class DefaultIssuerListDelegateTest( } private fun createIssuerListDelegate( - configuration: TestIssuerListConfiguration = getDefaultTestIssuerListConfigurationBuilder().build() + configuration: CheckoutConfiguration = createCheckoutConfiguration(), ) = DefaultIssuerListDelegate( observerRepository = PaymentObserverRepository(), - componentParams = IssuerListComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = IssuerListComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ), paymentMethod = PaymentMethod(), order = TEST_ORDER, analyticsRepository = analyticsRepository, submitHandler = submitHandler, typedPaymentMethodFactory = { TestIssuerPaymentMethod() }, - componentStateFactory = { data, isInputValid, isReady -> TestIssuerComponentState(data, isInputValid, isReady) } + componentStateFactory = { data, isInputValid, isReady -> + TestIssuerComponentState( + data, + isInputValid, + isReady, + ) + }, ) - private fun getDefaultTestIssuerListConfigurationBuilder() = TestIssuerListConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY_1 - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: TestIssuerListConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + val issuerListConfiguration = TestIssuerListConfiguration.Builder(shopperLocale, environment, clientKey) + .apply(configuration) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, issuerListConfiguration) + } companion object { + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") private const val TEST_CHECKOUT_ATTEMPT_ID = "TEST_CHECKOUT_ATTEMPT_ID" diff --git a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt index cbdef97afb..f4d53c0e51 100644 --- a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt +++ b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/internal/ui/model/IssuerListComponentParamsMapperTest.kt @@ -9,9 +9,15 @@ package com.adyen.checkout.issuerlist.internal.ui.model import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParams import com.adyen.checkout.components.core.internal.ui.model.AnalyticsParamsLevel -import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams +import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType @@ -25,11 +31,20 @@ import java.util.Locale internal class IssuerListComponentParamsMapperTest { + private val issuerListComponentParamsMapper = IssuerListComponentParamsMapper(CommonComponentParamsMapper()) + @Test - fun `when parent configuration is null and custom issuer list configuration fields are null then all fields should match`() { - val issuerListConfiguration = getTestIssuerListConfigurationBuilder().build() + fun `when drop-in override params are null and custom issuer list configuration fields are null then all fields should match`() { + val configuration = createCheckoutConfiguration() - val params = IssuerListComponentParamsMapper(null, null).mapToParams(issuerListConfiguration, null) + val params = issuerListComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ) val expected = getIssuerListComponentParams() @@ -37,21 +52,24 @@ internal class IssuerListComponentParamsMapperTest { } @Test - fun `when parent configuration is null and custom issuer list configuration fields are set then all fields should match`() { - val issuerListConfiguration = TestIssuerListConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 - ) - .setHideIssuerLogos(true) - .setViewType(IssuerListViewType.SPINNER_VIEW) - .setSubmitButtonVisible(false) - .build() + fun `when drop-in override params are null and custom issuer list configuration fields are set then all fields should match`() { + val configuration = createCheckoutConfiguration { + setHideIssuerLogos(true) + setViewType(IssuerListViewType.SPINNER_VIEW) + setSubmitButtonVisible(false) + } - val params = IssuerListComponentParamsMapper(null, null).mapToParams(issuerListConfiguration, null) + val params = issuerListComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ) - val expected = IssuerListComponentParams( - shopperLocale = Locale.US, + val expected = getIssuerListComponentParams( + shopperLocale = DEVICE_LOCALE, environment = Environment.TEST, clientKey = TEST_CLIENT_KEY_1, analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), @@ -59,40 +77,44 @@ internal class IssuerListComponentParamsMapperTest { viewType = IssuerListViewType.SPINNER_VIEW, hideIssuerLogos = true, amount = null, - isSubmitButtonVisible = false + isSubmitButtonVisible = false, ) assertEquals(expected, params) } @Test - fun `when parent configuration is set then parent configuration fields should override issuer list configuration fields`() { - val issuerListConfiguration = TestIssuerListConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 - ) - .setHideIssuerLogos(true) - .setViewType(IssuerListViewType.SPINNER_VIEW) - .build() - - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = GenericComponentParams( + fun `when drop-in override params are set then they should override issuer list configuration fields`() { + val configuration = CheckoutConfiguration( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.NONE), - isCreatedByDropIn = true, amount = Amount( currency = "XCD", - value = 4_00L - ) - ) + value = 4_00L, + ), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.NONE), + ) { + val issuerListConfiguration = TestIssuerListConfiguration.Builder(shopperLocale, environment, clientKey) + .setHideIssuerLogos(true) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setAmount(Amount("USD", 1L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, issuerListConfiguration) + } - val params = IssuerListComponentParamsMapper(overrideParams, null).mapToParams(issuerListConfiguration, null) + val dropInOverrideParams = DropInOverrideParams(Amount("CAD", 123L), null) + val params = issuerListComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ) - val expected = IssuerListComponentParams( + val expected = getIssuerListComponentParams( shopperLocale = Locale.GERMAN, environment = Environment.EUROPE, clientKey = TEST_CLIENT_KEY_2, @@ -101,10 +123,10 @@ internal class IssuerListComponentParamsMapperTest { viewType = IssuerListViewType.SPINNER_VIEW, hideIssuerLogos = true, amount = Amount( - currency = "XCD", - value = 4_00L + currency = "CAD", + value = 123L, ), - isSubmitButtonVisible = true + isSubmitButtonVisible = true, ) assertEquals(expected, params) @@ -112,60 +134,160 @@ internal class IssuerListComponentParamsMapperTest { @ParameterizedTest @MethodSource("amountSource") - fun `amount should match value set in sessions if it exists, then should match drop in value, then configuration`( + fun `amount should match value set in sessions then drop in then component configuration`( configurationValue: Amount, dropInValue: Amount?, sessionsValue: Amount?, expectedValue: Amount ) { - val issuerListConfiguration = getTestIssuerListConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) + + val dropInOverrideParams = dropInValue?.let { DropInOverrideParams(it, null) } + val sessionParams = createSessionParams( + amount = sessionsValue, + ) + val params = issuerListComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = sessionParams, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ) + + val expected = getIssuerListComponentParams( + amount = expectedValue, + isCreatedByDropIn = dropInOverrideParams != null, + ) - // this is in practice DropInComponentParams, but we don't have access to it in this module and any - // ComponentParams class can work - val overrideParams = dropInValue?.let { getIssuerListComponentParams().copy(amount = it) } + assertEquals(expected, params) + } - val params = IssuerListComponentParamsMapper(overrideParams, null).mapToParams( - issuerListConfiguration, - sessionParams = SessionParams( - enableStoreDetails = null, - installmentConfiguration = null, - amount = sessionsValue, - returnUrl = "", - ) + @ParameterizedTest + @MethodSource("shopperLocaleSource") + fun `shopper locale should match value set in configuration then sessions then device locale`( + configurationValue: Locale?, + sessionsValue: Locale?, + deviceLocaleValue: Locale, + expectedValue: Locale, + ) { + val configuration = createCheckoutConfiguration(shopperLocale = configurationValue) + + val sessionParams = createSessionParams( + shopperLocale = sessionsValue, ) - val expected = getIssuerListComponentParams().copy(amount = expectedValue) + val params = issuerListComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = deviceLocaleValue, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ) + + val expected = getIssuerListComponentParams( + shopperLocale = expectedValue, + ) assertEquals(expected, params) } - private fun getTestIssuerListConfigurationBuilder(): TestIssuerListConfiguration.Builder { - return TestIssuerListConfiguration.Builder( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1 + @Test + fun `environment and client key should match value set in sessions`() { + val configuration = createCheckoutConfiguration() + + val sessionParams = createSessionParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, + ) + + val params = issuerListComponentParamsMapper.mapToParams( + checkoutConfiguration = configuration, + deviceLocale = DEVICE_LOCALE, + dropInOverrideParams = null, + componentSessionParams = sessionParams, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + hideIssuerLogosDefaultValue = false, + ) + + val expected = getIssuerListComponentParams( + environment = Environment.INDIA, + clientKey = TEST_CLIENT_KEY_2, ) + + assertEquals(expected, params) } - private fun getIssuerListComponentParams(): IssuerListComponentParams { + private fun createCheckoutConfiguration( + amount: Amount? = null, + shopperLocale: Locale? = null, + configuration: TestIssuerListConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY_1, + amount = amount, + ) { + val issuerListConfiguration = TestIssuerListConfiguration.Builder(shopperLocale, environment, clientKey) + .apply(configuration) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, issuerListConfiguration) + } + + @Suppress("LongParameterList") + private fun createSessionParams( + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + enableStoreDetails: Boolean? = null, + installmentConfiguration: SessionInstallmentConfiguration? = null, + showRemovePaymentMethodButton: Boolean? = null, + amount: Amount? = null, + returnUrl: String? = "", + shopperLocale: Locale? = null, + ) = SessionParams( + environment = environment, + clientKey = clientKey, + enableStoreDetails = enableStoreDetails, + installmentConfiguration = installmentConfiguration, + showRemovePaymentMethodButton = showRemovePaymentMethodButton, + amount = amount, + returnUrl = returnUrl, + shopperLocale = shopperLocale, + ) + + @Suppress("LongParameterList") + private fun getIssuerListComponentParams( + shopperLocale: Locale = DEVICE_LOCALE, + environment: Environment = Environment.TEST, + clientKey: String = TEST_CLIENT_KEY_1, + analyticsParams: AnalyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), + isCreatedByDropIn: Boolean = false, + amount: Amount? = null, + isSubmitButtonVisible: Boolean = true, + viewType: IssuerListViewType = IssuerListViewType.RECYCLER_VIEW, + hideIssuerLogos: Boolean = false, + ): IssuerListComponentParams { return IssuerListComponentParams( - shopperLocale = Locale.US, - environment = Environment.TEST, - clientKey = TEST_CLIENT_KEY_1, - analyticsParams = AnalyticsParams(AnalyticsParamsLevel.ALL), - isCreatedByDropIn = false, - viewType = IssuerListViewType.RECYCLER_VIEW, - hideIssuerLogos = false, - amount = null, - isSubmitButtonVisible = true + commonComponentParams = CommonComponentParams( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + analyticsParams = analyticsParams, + isCreatedByDropIn = isCreatedByDropIn, + amount = amount, + ), + isSubmitButtonVisible = isSubmitButtonVisible, + viewType = viewType, + hideIssuerLogos = hideIssuerLogos, ) } companion object { + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty" + private val DEVICE_LOCALE = Locale("nl", "NL") @JvmStatic fun amountSource() = listOf( @@ -174,5 +296,14 @@ internal class IssuerListComponentParamsMapperTest { arguments(Amount("EUR", 100), Amount("USD", 200), null, Amount("USD", 200)), arguments(Amount("EUR", 100), null, null, Amount("EUR", 100)), ) + + @JvmStatic + fun shopperLocaleSource() = listOf( + // configurationValue, sessionsValue, deviceLocaleValue, expectedValue + arguments(null, null, Locale.US, Locale.US), + arguments(Locale.GERMAN, null, Locale.US, Locale.GERMAN), + arguments(null, Locale.CHINESE, Locale.US, Locale.CHINESE), + arguments(Locale.GERMAN, Locale.CHINESE, Locale.US, Locale.GERMAN), + ) } } diff --git a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/utils/TestIssuerListConfiguration.kt b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/utils/TestIssuerListConfiguration.kt index d3e1e5219b..058c9519f9 100644 --- a/issuer-list/src/test/java/com/adyen/checkout/issuerlist/utils/TestIssuerListConfiguration.kt +++ b/issuer-list/src/test/java/com/adyen/checkout/issuerlist/utils/TestIssuerListConfiguration.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.issuerlist.utils -import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration @@ -22,7 +21,7 @@ import java.util.Locale class TestIssuerListConfiguration @Suppress("LongParameterList") private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -33,19 +32,12 @@ private constructor( override val genericActionConfiguration: GenericActionConfiguration, ) : IssuerListConfiguration() { - class Builder : IssuerListBuilder { + class Builder(shopperLocale: Locale?, environment: Environment, clientKey: String) : + IssuerListBuilder(environment, clientKey) { - constructor(context: Context, environment: Environment, clientKey: String) : super( - context, - environment, - clientKey - ) - - constructor( - shopperLocale: Locale, - environment: Environment, - clientKey: String - ) : super(shopperLocale, environment, clientKey) + init { + shopperLocale?.let { setShopperLocale(it) } + } public override fun buildInternal(): TestIssuerListConfiguration { return TestIssuerListConfiguration( diff --git a/mbway/src/main/java/com/adyen/checkout/mbway/MBWayComponent.kt b/mbway/src/main/java/com/adyen/checkout/mbway/MBWayComponent.kt index 7a9c3543f0..42ecad0258 100644 --- a/mbway/src/main/java/com/adyen/checkout/mbway/MBWayComponent.kt +++ b/mbway/src/main/java/com/adyen/checkout/mbway/MBWayComponent.kt @@ -20,8 +20,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.mbway.internal.provider.MBWayComponentProvider import com.adyen.checkout.mbway.internal.ui.MBWayDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate @@ -74,24 +74,24 @@ class MBWayComponent internal constructor( override fun isConfirmationRequired(): Boolean = mbWayDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? MBWayDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } mbWayDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = MBWayComponentProvider() diff --git a/mbway/src/main/java/com/adyen/checkout/mbway/MBWayConfiguration.kt b/mbway/src/main/java/com/adyen/checkout/mbway/MBWayConfiguration.kt index 44a9eb4743..dbbb8f2d72 100644 --- a/mbway/src/main/java/com/adyen/checkout/mbway/MBWayConfiguration.kt +++ b/mbway/src/main/java/com/adyen/checkout/mbway/MBWayConfiguration.kt @@ -12,9 +12,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -25,7 +28,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class MBWayConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -43,6 +46,22 @@ class MBWayConfiguration private constructor( private var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -50,14 +69,15 @@ class MBWayConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -66,7 +86,7 @@ class MBWayConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) /** @@ -94,3 +114,38 @@ class MBWayConfiguration private constructor( } } } + +fun CheckoutConfiguration.mbWay( + configuration: @CheckoutConfigurationMarker MBWayConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = MBWayConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.MB_WAY, config) + return this +} + +fun CheckoutConfiguration.getMBWayConfiguration(): MBWayConfiguration? { + return getConfiguration(PaymentMethodTypes.MB_WAY) +} + +internal fun MBWayConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.MB_WAY, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/mbway/src/main/java/com/adyen/checkout/mbway/internal/provider/MBWayComponentProvider.kt b/mbway/src/main/java/com/adyen/checkout/mbway/internal/provider/MBWayComponentProvider.kt index df40670355..8577f025d4 100644 --- a/mbway/src/main/java/com/adyen/checkout/mbway/internal/provider/MBWayComponentProvider.kt +++ b/mbway/src/main/java/com/adyen/checkout/mbway/internal/provider/MBWayComponentProvider.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -28,16 +29,19 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.mbway.MBWayComponent import com.adyen.checkout.mbway.MBWayComponentState import com.adyen.checkout.mbway.MBWayConfiguration +import com.adyen.checkout.mbway.getMBWayConfiguration import com.adyen.checkout.mbway.internal.ui.DefaultMBWayDelegate +import com.adyen.checkout.mbway.toCheckoutConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -52,31 +56,29 @@ import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class MBWayComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< MBWayComponent, MBWayConfiguration, MBWayComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< MBWayComponent, MBWayConfiguration, MBWayComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: MBWayConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -85,7 +87,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = checkoutConfiguration.getMBWayConfiguration(), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -94,7 +102,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -108,8 +116,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -130,6 +138,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: MBWayConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String?, + ): MBWayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -137,7 +169,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: MBWayConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -145,10 +177,14 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = checkoutConfiguration.getMBWayConfiguration(), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -159,7 +195,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -173,8 +209,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -190,7 +226,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( @@ -214,6 +250,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: MBWayConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): MBWayComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegate.kt b/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegate.kt index 962bbfde51..1070f294a6 100644 --- a/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegate.kt +++ b/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegate.kt @@ -21,8 +21,8 @@ import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParam import com.adyen.checkout.components.core.internal.util.CountryInfo import com.adyen.checkout.components.core.internal.util.CountryUtils import com.adyen.checkout.components.core.paymentmethod.MBWayPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.mbway.MBWayComponentState import com.adyen.checkout.mbway.internal.ui.model.MBWayInputData import com.adyen.checkout.mbway.internal.ui.model.MBWayOutputData @@ -75,7 +75,7 @@ internal class DefaultMBWayDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -92,7 +92,7 @@ internal class DefaultMBWayDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -110,7 +110,7 @@ internal class DefaultMBWayDelegate( } private fun onInputDataChanged() { - Logger.v(TAG, "onInputDataChanged") + adyenLog(AdyenLogLevel.VERBOSE) { "onInputDataChanged" } val outputData = createOutputData() outputDataChanged(outputData) updateComponentState(outputData) @@ -137,7 +137,7 @@ internal class DefaultMBWayDelegate( val paymentMethod = MBWayPaymentMethod( type = MBWayPaymentMethod.PAYMENT_METHOD_TYPE, checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), - telephoneNumber = outputData.mobilePhoneNumberFieldState.value + telephoneNumber = outputData.mobilePhoneNumberFieldState.value, ) val paymentComponentData = PaymentComponentData( @@ -149,7 +149,7 @@ internal class DefaultMBWayDelegate( return MBWayComponentState( data = paymentComponentData, isInputValid = outputData.isValid, - isReady = true + isReady = true, ) } @@ -177,8 +177,6 @@ internal class DefaultMBWayDelegate( } companion object { - private val TAG = LogUtil.getTag() - private const val ISO_CODE_PORTUGAL = "PT" private const val ISO_CODE_SPAIN = "ES" diff --git a/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/view/MbWayView.kt b/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/view/MbWayView.kt index 1974a93bf0..96a3bd898b 100644 --- a/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/view/MbWayView.kt +++ b/mbway/src/main/java/com/adyen/checkout/mbway/internal/ui/view/MbWayView.kt @@ -18,8 +18,8 @@ import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.internal.util.CountryInfo import com.adyen.checkout.components.core.internal.util.CountryUtils -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.mbway.R import com.adyen.checkout.mbway.databinding.MbwayViewBinding import com.adyen.checkout.mbway.internal.ui.MBWayDelegate @@ -73,7 +73,7 @@ internal class MbWayView @JvmOverloads constructor( binding.textInputLayoutMobileNumber.hideError() } else if (mobilePhoneNumberValidation is Validation.Invalid) { binding.textInputLayoutMobileNumber.showError( - localizedContext.getString(mobilePhoneNumberValidation.reason) + localizedContext.getString(mobilePhoneNumberValidation.reason), ) } } @@ -100,11 +100,11 @@ internal class MbWayView @JvmOverloads constructor( } override fun highlightValidationErrors() { - Logger.d(TAG, "highlightValidationErrors") + adyenLog(AdyenLogLevel.DEBUG) { "highlightValidationErrors" } val mobilePhoneNumberValidation = delegate.outputData.mobilePhoneNumberFieldState.validation if (mobilePhoneNumberValidation is Validation.Invalid) { binding.textInputLayoutMobileNumber.showError( - localizedContext.getString(mobilePhoneNumberValidation.reason) + localizedContext.getString(mobilePhoneNumberValidation.reason), ) } } @@ -122,11 +122,7 @@ internal class MbWayView @JvmOverloads constructor( isoCode = it.isoCode, countryName = CountryUtils.getCountryName(it.isoCode, delegate.componentParams.shopperLocale), callingCode = it.callingCode, - emoji = it.emoji + emoji = it.emoji, ) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/mbway/src/test/java/com/adyen/checkout/mbway/MBWayComponentTest.kt b/mbway/src/test/java/com/adyen/checkout/mbway/MBWayComponentTest.kt index c9cb53ec71..a2c7782927 100644 --- a/mbway/src/test/java/com/adyen/checkout/mbway/MBWayComponentTest.kt +++ b/mbway/src/test/java/com/adyen/checkout/mbway/MBWayComponentTest.kt @@ -15,10 +15,9 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.mbway.internal.ui.MBWayDelegate import com.adyen.checkout.mbway.internal.ui.MbWayComponentViewType +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -40,7 +39,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class MBWayComponentTest( @Mock private val mbWayDelegate: MBWayDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -61,7 +60,6 @@ internal class MBWayComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/mbway/src/test/java/com/adyen/checkout/mbway/MBWayConfigurationTest.kt b/mbway/src/test/java/com/adyen/checkout/mbway/MBWayConfigurationTest.kt new file mode 100644 index 0000000000..ef8eb2c4ee --- /dev/null +++ b/mbway/src/test/java/com/adyen/checkout/mbway/MBWayConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.mbway + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class MBWayConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + mbWay { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getMBWayConfiguration() + + val expected = MBWayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = MBWayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualMbWayConfig = actual.getMBWayConfiguration() + assertEquals(config.shopperLocale, actualMbWayConfig?.shopperLocale) + assertEquals(config.environment, actualMbWayConfig?.environment) + assertEquals(config.clientKey, actualMbWayConfig?.clientKey) + assertEquals(config.amount, actualMbWayConfig?.amount) + assertEquals(config.analyticsConfiguration, actualMbWayConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualMbWayConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/mbway/src/test/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegateTest.kt b/mbway/src/test/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegateTest.kt index a39db78f7b..3f18312f7f 100644 --- a/mbway/src/test/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegateTest.kt +++ b/mbway/src/test/java/com/adyen/checkout/mbway/internal/ui/DefaultMBWayDelegateTest.kt @@ -10,15 +10,19 @@ package com.adyen.checkout.mbway.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment import com.adyen.checkout.mbway.MBWayComponentState import com.adyen.checkout.mbway.MBWayConfiguration +import com.adyen.checkout.mbway.getMBWayConfiguration import com.adyen.checkout.mbway.internal.ui.model.MBWayOutputData +import com.adyen.checkout.mbway.mbWay import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -179,9 +183,7 @@ internal class DefaultMBWayDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultMBWayConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createMBWayDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -207,9 +209,9 @@ internal class DefaultMBWayDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createMBWayDelegate( - configuration = getDefaultMBWayConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -218,9 +220,9 @@ internal class DefaultMBWayDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createMBWayDelegate( - configuration = getDefaultMBWayConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -275,21 +277,33 @@ internal class DefaultMBWayDelegateTest( } private fun createMBWayDelegate( - configuration: MBWayConfiguration = getDefaultMBWayConfigurationBuilder().build() + configuration: CheckoutConfiguration = createCheckoutConfiguration(), ) = DefaultMBWayDelegate( observerRepository = PaymentObserverRepository(), paymentMethod = PaymentMethod(), order = TEST_ORDER, - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getMBWayConfiguration(), + ), analyticsRepository = analyticsRepository, submitHandler = submitHandler, ) - private fun getDefaultMBWayConfigurationBuilder() = MBWayConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: MBWayConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + mbWay(configuration) + } companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" diff --git a/molpay/src/main/java/com/adyen/checkout/molpay/MolpayConfiguration.kt b/molpay/src/main/java/com/adyen/checkout/molpay/MolpayConfiguration.kt index f619b836ac..af027908b4 100644 --- a/molpay/src/main/java/com/adyen/checkout/molpay/MolpayConfiguration.kt +++ b/molpay/src/main/java/com/adyen/checkout/molpay/MolpayConfiguration.kt @@ -11,6 +11,8 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -23,7 +25,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class MolpayConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -39,6 +41,22 @@ class MolpayConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -46,14 +64,15 @@ class MolpayConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -80,3 +99,46 @@ class MolpayConfiguration private constructor( } } } + +fun CheckoutConfiguration.molpay( + configuration: @CheckoutConfigurationMarker MolpayConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = MolpayConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + + MolpayComponent.PAYMENT_METHOD_TYPES.forEach { key -> + addConfiguration(key, config) + } + + return this +} + +fun CheckoutConfiguration.getMolpayConfiguration(): MolpayConfiguration? { + return MolpayComponent.PAYMENT_METHOD_TYPES.firstNotNullOfOrNull { key -> + getConfiguration(key) + } +} + +internal fun MolpayConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + MolpayComponent.PAYMENT_METHOD_TYPES.forEach { key -> + addConfiguration(key, this@toCheckoutConfiguration) + } + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/molpay/src/main/java/com/adyen/checkout/molpay/internal/provider/MolpayComponentProvider.kt b/molpay/src/main/java/com/adyen/checkout/molpay/internal/provider/MolpayComponentProvider.kt index 7b54ac9668..45885eecb0 100644 --- a/molpay/src/main/java/com/adyen/checkout/molpay/internal/provider/MolpayComponentProvider.kt +++ b/molpay/src/main/java/com/adyen/checkout/molpay/internal/provider/MolpayComponentProvider.kt @@ -11,28 +11,28 @@ package com.adyen.checkout.molpay.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.MolpayPaymentMethod import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.molpay.MolpayComponent import com.adyen.checkout.molpay.MolpayComponentState import com.adyen.checkout.molpay.MolpayConfiguration +import com.adyen.checkout.molpay.getMolpayConfiguration +import com.adyen.checkout.molpay.toCheckoutConfiguration class MolpayComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider( componentClass = MolpayComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -57,4 +57,12 @@ constructor( override fun createPaymentMethod() = MolpayPaymentMethod() override fun getSupportedPaymentMethods(): List = MolpayComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): MolpayConfiguration? { + return checkoutConfiguration.getMolpayConfiguration() + } + + override fun getCheckoutConfiguration(configuration: MolpayConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/molpay/src/test/java/com/adyen/checkout/molpay/MolpayConfigurationTest.kt b/molpay/src/test/java/com/adyen/checkout/molpay/MolpayConfigurationTest.kt new file mode 100644 index 0000000000..4483c39844 --- /dev/null +++ b/molpay/src/test/java/com/adyen/checkout/molpay/MolpayConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.molpay + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class MolpayConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + molpay { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getMolpayConfiguration() + + val expected = MolpayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = MolpayConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualMolpayConfig = actual.getMolpayConfiguration() + assertEquals(config.shopperLocale, actualMolpayConfig?.shopperLocale) + assertEquals(config.environment, actualMolpayConfig?.environment) + assertEquals(config.clientKey, actualMolpayConfig?.clientKey) + assertEquals(config.amount, actualMolpayConfig?.amount) + assertEquals(config.analyticsConfiguration, actualMolpayConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualMolpayConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualMolpayConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualMolpayConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponent.kt b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponent.kt index 01bfb27805..8cae9ef2ef 100644 --- a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponent.kt +++ b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponent.kt @@ -21,8 +21,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.paymentmethod.IssuerListPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.onlinebankingcore.internal.ui.OnlineBankingDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -74,23 +74,20 @@ abstract class OnlineBankingComponent< override fun isConfirmationRequired(): Boolean = onlineBankingDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? OnlineBankingDelegate<*, *>)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } onlineBankingDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingConfiguration.kt b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingConfiguration.kt index d96ad22fd4..fd30b40c84 100644 --- a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingConfiguration.kt +++ b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingConfiguration.kt @@ -32,6 +32,9 @@ abstract class OnlineBankingConfiguration : Configuration, ButtonConfiguration { protected open var isSubmitButtonVisible: Boolean? = null + protected constructor(environment: Environment, clientKey: String) : super(environment, clientKey) + + @Deprecated("You can omit the context parameter") protected constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, diff --git a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt index 10dc9eacb3..9ba3010645 100644 --- a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt +++ b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/provider/OnlineBankingComponentProvider.kt @@ -17,6 +17,7 @@ import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentComponentData @@ -32,13 +33,14 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.components.core.paymentmethod.IssuerListPaymentMethod import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.onlinebankingcore.internal.OnlineBankingComponent import com.adyen.checkout.onlinebankingcore.internal.OnlineBankingConfiguration import com.adyen.checkout.onlinebankingcore.internal.ui.DefaultOnlineBankingDelegate @@ -55,7 +57,7 @@ import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import com.adyen.checkout.ui.core.internal.util.PdfOpener -@Suppress("ktlint:standard:type-parameter-list-spacing") +@Suppress("TooManyFunctions", "ktlint:standard:type-parameter-list-spacing") abstract class OnlineBankingComponentProvider< ComponentT : OnlineBankingComponent, ConfigurationT : OnlineBankingConfiguration, @@ -65,26 +67,24 @@ abstract class OnlineBankingComponentProvider< @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( private val componentClass: Class, - overrideComponentParams: ComponentParams?, - overrideSessionParams: SessionParams?, + private val dropInOverrideParams: DropInOverrideParams?, private val analyticsRepository: AnalyticsRepository?, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider>, SessionPaymentComponentProvider< ComponentT, ConfigurationT, ComponentStateT, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: ConfigurationT, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -93,7 +93,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = getConfiguration(checkoutConfiguration), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -102,7 +108,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -117,11 +123,11 @@ constructor( termsAndConditionsUrl = getTermsAndConditionsUrl(), submitHandler = SubmitHandler(savedStateHandle), paymentMethodFactory = { createPaymentMethod() }, - componentStateFactory = ::createComponentState + componentStateFactory = ::createComponentState, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -131,9 +137,9 @@ constructor( genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent( genericActionDelegate, - onlineBankingDelegate + onlineBankingDelegate, ), - componentEventHandler = DefaultComponentEventHandler() + componentEventHandler = DefaultComponentEventHandler(), ) } @@ -144,6 +150,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: ConfigurationT, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): ComponentT { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = getCheckoutConfiguration(configuration), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -151,7 +181,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: ConfigurationT, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -159,10 +189,14 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = getConfiguration(checkoutConfiguration), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -173,7 +207,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -188,11 +222,11 @@ constructor( termsAndConditionsUrl = getTermsAndConditionsUrl(), submitHandler = SubmitHandler(savedStateHandle), paymentMethodFactory = { createPaymentMethod() }, - componentStateFactory = ::createComponentState + componentStateFactory = ::createComponentState, ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -207,7 +241,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -219,9 +253,9 @@ constructor( genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent( genericActionDelegate, - onlineBankingDelegate + onlineBankingDelegate, ), - componentEventHandler = sessionComponentEventHandler + componentEventHandler = sessionComponentEventHandler, ) } @@ -233,6 +267,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: ConfigurationT, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): ComponentT { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = getCheckoutConfiguration(configuration), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + protected abstract fun createComponentState( data: PaymentComponentData, isInputValid: Boolean, @@ -252,6 +310,10 @@ constructor( protected abstract fun getTermsAndConditionsUrl(): String + protected abstract fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): ConfigurationT? + + protected abstract fun getCheckoutConfiguration(configuration: ConfigurationT): CheckoutConfiguration + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegate.kt b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegate.kt index 7a8c18ebfc..d4633d09de 100644 --- a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegate.kt +++ b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegate.kt @@ -22,9 +22,9 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParams import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.paymentmethod.IssuerListPaymentMethod +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.onlinebankingcore.internal.ui.model.OnlineBankingInputData import com.adyen.checkout.onlinebankingcore.internal.ui.model.OnlineBankingModel import com.adyen.checkout.onlinebankingcore.internal.ui.model.OnlineBankingOutputData @@ -96,7 +96,7 @@ internal class DefaultOnlineBankingDelegate< } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -191,8 +191,4 @@ internal class DefaultOnlineBankingDelegate< override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/OnlineBankingView.kt b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/OnlineBankingView.kt index 8771ed0f0e..95ebab4cb5 100644 --- a/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/OnlineBankingView.kt +++ b/online-banking-core/src/main/java/com/adyen/checkout/onlinebankingcore/internal/ui/OnlineBankingView.kt @@ -15,8 +15,8 @@ import android.widget.AdapterView import android.widget.LinearLayout import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.onlinebankingcore.R import com.adyen.checkout.onlinebankingcore.databinding.OnlineBankingViewBinding import com.adyen.checkout.onlinebankingcore.internal.ui.model.OnlineBankingModel @@ -36,7 +36,7 @@ internal class OnlineBankingView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -67,8 +67,9 @@ internal class OnlineBankingView @JvmOverloads constructor( inputType = 0 setAdapter(issuersAdapter) onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> - Logger.d(TAG, "onItemSelected - ${issuersAdapter.getItem(position).name}") - onlineBankingDelegate.updateInputData { selectedIssuer = issuersAdapter.getItem(position) } + val item = issuersAdapter.getItem(position) + adyenLog(AdyenLogLevel.DEBUG) { "onItemSelected - ${item.name}" } + onlineBankingDelegate.updateInputData { selectedIssuer = item } binding.textInputLayoutOnlineBanking.hideError() } } @@ -78,7 +79,7 @@ internal class OnlineBankingView @JvmOverloads constructor( } override fun highlightValidationErrors() { - Logger.d(TAG, "highlightValidationErrors") + adyenLog(AdyenLogLevel.DEBUG) { "highlightValidationErrors" } val output = onlineBankingDelegate.outputData val selectedIssuersValidation = output.selectedIssuerField.validation if (!selectedIssuersValidation.isValid()) { @@ -94,12 +95,12 @@ internal class OnlineBankingView @JvmOverloads constructor( binding.textInputLayoutOnlineBanking .setLocalizedHintFromStyle( R.style.AdyenCheckout_OnlineBanking_TermsAndConditionsInputLayout, - localizedContext + localizedContext, ) binding.textviewTermsAndConditions.setLocalizedTextFromStyle( R.style.AdyenCheckout_OnlineBanking_TermsAndConditionsTextView, localizedContext, - formatHyperLink = true + formatHyperLink = true, ) } @@ -110,8 +111,4 @@ internal class OnlineBankingView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponentTest.kt b/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponentTest.kt index 92974245dc..f5f4c6d128 100644 --- a/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponentTest.kt +++ b/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/OnlineBankingComponentTest.kt @@ -15,13 +15,12 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.onlinebankingcore.internal.ui.OnlineBankingComponentViewType import com.adyen.checkout.onlinebankingcore.internal.ui.OnlineBankingDelegate import com.adyen.checkout.onlinebankingcore.utils.TestOnlineBankingComponent import com.adyen.checkout.onlinebankingcore.utils.TestOnlineBankingComponentState import com.adyen.checkout.onlinebankingcore.utils.TestOnlineBankingPaymentMethod +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -43,7 +42,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class OnlineBankingComponentTest( @Mock private val onlineBankingDelegate: OnlineBankingDelegate< TestOnlineBankingPaymentMethod, @@ -70,7 +69,6 @@ internal class OnlineBankingComponentTest( actionHandlingComponent, componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -126,7 +124,7 @@ internal class OnlineBankingComponentTest( onlineBankingDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) component.viewFlow.test { @@ -147,7 +145,7 @@ internal class OnlineBankingComponentTest( onlineBankingDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) component.viewFlow.test { diff --git a/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegateTest.kt b/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegateTest.kt index 013872a08e..a6505d1887 100644 --- a/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegateTest.kt +++ b/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/internal/ui/DefaultOnlineBankingDelegateTest.kt @@ -11,11 +11,13 @@ package com.adyen.checkout.onlinebankingcore.internal.ui import android.content.Context import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.core.Environment import com.adyen.checkout.onlinebankingcore.internal.ui.model.OnlineBankingModel import com.adyen.checkout.onlinebankingcore.internal.ui.model.OnlineBankingOutputData @@ -60,7 +62,7 @@ internal class DefaultOnlineBankingDelegateTest( private lateinit var delegate: DefaultOnlineBankingDelegate< TestOnlineBankingPaymentMethod, - TestOnlineBankingComponentState + TestOnlineBankingComponentState, > @BeforeEach @@ -139,9 +141,7 @@ internal class DefaultOnlineBankingDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultTestOnlineBankingConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createOnlineBankingDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -189,9 +189,9 @@ internal class DefaultOnlineBankingDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createOnlineBankingDelegate( - configuration = getDefaultTestOnlineBankingConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) assertFalse(delegate.shouldShowSubmitButton()) @@ -200,9 +200,9 @@ internal class DefaultOnlineBankingDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createOnlineBankingDelegate( - configuration = getDefaultTestOnlineBankingConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -256,7 +256,7 @@ internal class DefaultOnlineBankingDelegateTest( } private fun createOnlineBankingDelegate( - configuration: TestOnlineBankingConfiguration = getDefaultTestOnlineBankingConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: OrderRequest? = TEST_ORDER, ) = DefaultOnlineBankingDelegate( observerRepository = PaymentObserverRepository(), @@ -264,7 +264,14 @@ internal class DefaultOnlineBankingDelegateTest( paymentMethod = PaymentMethod(), order = order, analyticsRepository = analyticsRepository, - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getConfiguration(TEST_CONFIGURATION_KEY), + ), termsAndConditionsUrl = TEST_URL, paymentMethodFactory = { TestOnlineBankingPaymentMethod() }, submitHandler = submitHandler, @@ -272,18 +279,28 @@ internal class DefaultOnlineBankingDelegateTest( TestOnlineBankingComponentState( data = data, isInputValid = isInputValid, - isReady = isReady + isReady = isReady, ) - } + }, ) - private fun getDefaultTestOnlineBankingConfigurationBuilder() = TestOnlineBankingConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: TestOnlineBankingConfiguration.Builder.() -> Unit = {} + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + val testConfiguration = TestOnlineBankingConfiguration.Builder(shopperLocale, environment, clientKey) + .apply(configuration) + .build() + addConfiguration(TEST_CONFIGURATION_KEY, testConfiguration) + } companion object { + private const val TEST_CONFIGURATION_KEY = "TEST_CONFIGURATION_KEY" private const val TEST_URL = "any-url" private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA") diff --git a/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/utils/TestOnlineBankingConfiguration.kt b/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/utils/TestOnlineBankingConfiguration.kt index 0f27d6d4ba..9134470363 100644 --- a/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/utils/TestOnlineBankingConfiguration.kt +++ b/online-banking-core/src/test/java/com/adyen/checkout/onlinebankingcore/utils/TestOnlineBankingConfiguration.kt @@ -8,7 +8,6 @@ package com.adyen.checkout.onlinebankingcore.utils -import android.content.Context import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder @@ -21,7 +20,7 @@ import java.util.Locale @Parcelize internal class TestOnlineBankingConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -30,22 +29,15 @@ internal class TestOnlineBankingConfiguration private constructor( ) : Configuration, ButtonConfiguration { - class Builder : BaseConfigurationBuilder, ButtonConfigurationBuilder { + class Builder( + shopperLocale: Locale?, + environment: Environment, + clientKey: String + ) : BaseConfigurationBuilder(shopperLocale, environment, clientKey), + ButtonConfigurationBuilder { private var isSubmitButtonVisible: Boolean? = null - constructor(context: Context, environment: Environment, clientKey: String) : super( - context, - environment, - clientKey - ) - - constructor( - shopperLocale: Locale, - environment: Environment, - clientKey: String - ) : super(shopperLocale, environment, clientKey) - override fun setSubmitButtonVisible(isSubmitButtonVisible: Boolean): Builder { this.isSubmitButtonVisible = isSubmitButtonVisible return this diff --git a/online-banking-cz/build.gradle b/online-banking-cz/build.gradle index 4c352308fa..85e1887d24 100644 --- a/online-banking-cz/build.gradle +++ b/online-banking-cz/build.gradle @@ -37,4 +37,7 @@ dependencies { // Checkout api project(':action-core') api project(':online-banking-core') + + testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito } diff --git a/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfiguration.kt b/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfiguration.kt index 505bcc827a..d5bef8184d 100644 --- a/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfiguration.kt +++ b/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.onlinebankingcore.internal.OnlineBankingConfiguration import kotlinx.parcelize.Parcelize @@ -23,7 +26,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class OnlineBankingCZConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -37,6 +40,22 @@ class OnlineBankingCZConfiguration private constructor( */ class Builder : OnlineBankingConfigurationBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,14 +63,15 @@ class OnlineBankingCZConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -76,3 +96,38 @@ class OnlineBankingCZConfiguration private constructor( } } } + +fun CheckoutConfiguration.onlineBankingCZ( + configuration: @CheckoutConfigurationMarker OnlineBankingCZConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = OnlineBankingCZConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ONLINE_BANKING_CZ, config) + return this +} + +fun CheckoutConfiguration.getOnlineBankingCZConfiguration(): OnlineBankingCZConfiguration? { + return getConfiguration(PaymentMethodTypes.ONLINE_BANKING_CZ) +} + +internal fun OnlineBankingCZConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ONLINE_BANKING_CZ, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/internal/provider/OnlineBankingCZComponentProvider.kt b/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/internal/provider/OnlineBankingCZComponentProvider.kt index 1b12f54e99..467851265b 100644 --- a/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/internal/provider/OnlineBankingCZComponentProvider.kt +++ b/online-banking-cz/src/main/java/com/adyen/checkout/onlinebankingcz/internal/provider/OnlineBankingCZComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.onlinebankingcz.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.OnlineBankingCZPaymentMethod import com.adyen.checkout.onlinebankingcore.internal.provider.OnlineBankingComponentProvider import com.adyen.checkout.onlinebankingcore.internal.ui.OnlineBankingDelegate import com.adyen.checkout.onlinebankingcz.OnlineBankingCZComponent import com.adyen.checkout.onlinebankingcz.OnlineBankingCZComponentState import com.adyen.checkout.onlinebankingcz.OnlineBankingCZConfiguration +import com.adyen.checkout.onlinebankingcz.getOnlineBankingCZConfiguration +import com.adyen.checkout.onlinebankingcz.toCheckoutConfiguration class OnlineBankingCZComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : OnlineBankingComponentProvider< OnlineBankingCZComponent, OnlineBankingCZConfiguration, OnlineBankingCZPaymentMethod, - OnlineBankingCZComponentState + OnlineBankingCZComponentState, >( componentClass = OnlineBankingCZComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -69,7 +69,15 @@ constructor( delegate = delegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) } + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): OnlineBankingCZConfiguration? { + return checkoutConfiguration.getOnlineBankingCZConfiguration() + } + + override fun getCheckoutConfiguration(configuration: OnlineBankingCZConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/online-banking-cz/src/test/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfigurationTest.kt b/online-banking-cz/src/test/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfigurationTest.kt new file mode 100644 index 0000000000..df3416a8ce --- /dev/null +++ b/online-banking-cz/src/test/java/com/adyen/checkout/onlinebankingcz/OnlineBankingCZConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.onlinebankingcz + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class OnlineBankingCZConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + onlineBankingCZ { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getOnlineBankingCZConfiguration() + + val expected = OnlineBankingCZConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = OnlineBankingCZConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualCzConfig = actual.getOnlineBankingCZConfiguration() + assertEquals(config.shopperLocale, actualCzConfig?.shopperLocale) + assertEquals(config.environment, actualCzConfig?.environment) + assertEquals(config.clientKey, actualCzConfig?.clientKey) + assertEquals(config.amount, actualCzConfig?.amount) + assertEquals(config.analyticsConfiguration, actualCzConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualCzConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/online-banking-jp/build.gradle b/online-banking-jp/build.gradle index 119e4730d8..c3164191d3 100644 --- a/online-banking-jp/build.gradle +++ b/online-banking-jp/build.gradle @@ -32,4 +32,7 @@ android { dependencies { api project(':econtext') + + testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito } diff --git a/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfiguration.kt b/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfiguration.kt index b66f701e0d..5f172b421a 100644 --- a/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfiguration.kt +++ b/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.econtext.internal.EContextConfiguration import kotlinx.parcelize.Parcelize @@ -23,7 +26,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class OnlineBankingJPConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -37,6 +40,22 @@ class OnlineBankingJPConfiguration private constructor( */ class Builder : EContextConfiguration.Builder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,14 +63,15 @@ class OnlineBankingJPConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -76,3 +96,38 @@ class OnlineBankingJPConfiguration private constructor( } } } + +fun CheckoutConfiguration.onlineBankingJP( + configuration: @CheckoutConfigurationMarker OnlineBankingJPConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = OnlineBankingJPConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ECONTEXT_ONLINE, config) + return this +} + +fun CheckoutConfiguration.getOnlineBankingJPConfiguration(): OnlineBankingJPConfiguration? { + return getConfiguration(PaymentMethodTypes.ECONTEXT_ONLINE) +} + +internal fun OnlineBankingJPConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ECONTEXT_ONLINE, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/internal/provider/OnlineBankingJPComponentProvider.kt b/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/internal/provider/OnlineBankingJPComponentProvider.kt index 0fce6f39d7..3474600925 100644 --- a/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/internal/provider/OnlineBankingJPComponentProvider.kt +++ b/online-banking-jp/src/main/java/com/adyen/checkout/onlinebankingjp/internal/provider/OnlineBankingJPComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.onlinebankingjp.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.OnlineBankingJPPaymentMethod import com.adyen.checkout.econtext.internal.provider.EContextComponentProvider import com.adyen.checkout.econtext.internal.ui.EContextDelegate import com.adyen.checkout.onlinebankingjp.OnlineBankingJPComponent import com.adyen.checkout.onlinebankingjp.OnlineBankingJPComponentState import com.adyen.checkout.onlinebankingjp.OnlineBankingJPConfiguration +import com.adyen.checkout.onlinebankingjp.getOnlineBankingJPConfiguration +import com.adyen.checkout.onlinebankingjp.toCheckoutConfiguration class OnlineBankingJPComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : EContextComponentProvider< OnlineBankingJPComponent, OnlineBankingJPConfiguration, OnlineBankingJPPaymentMethod, - OnlineBankingJPComponentState + OnlineBankingJPComponentState, >( componentClass = OnlineBankingJPComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -65,6 +65,14 @@ constructor( return OnlineBankingJPPaymentMethod() } + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): OnlineBankingJPConfiguration? { + return checkoutConfiguration.getOnlineBankingJPConfiguration() + } + + override fun getCheckoutConfiguration(configuration: OnlineBankingJPConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } + override fun getSupportedPaymentMethods(): List { return OnlineBankingJPComponent.PAYMENT_METHOD_TYPES } diff --git a/online-banking-jp/src/test/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfigurationTest.kt b/online-banking-jp/src/test/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfigurationTest.kt new file mode 100644 index 0000000000..8c2b39b7a5 --- /dev/null +++ b/online-banking-jp/src/test/java/com/adyen/checkout/onlinebankingjp/OnlineBankingJPConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.onlinebankingjp + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class OnlineBankingJPConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + onlineBankingJP { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getOnlineBankingJPConfiguration() + + val expected = OnlineBankingJPConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = OnlineBankingJPConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualJpConfig = actual.getOnlineBankingJPConfiguration() + assertEquals(config.shopperLocale, actualJpConfig?.shopperLocale) + assertEquals(config.environment, actualJpConfig?.environment) + assertEquals(config.clientKey, actualJpConfig?.clientKey) + assertEquals(config.amount, actualJpConfig?.amount) + assertEquals(config.analyticsConfiguration, actualJpConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualJpConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfiguration.kt b/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfiguration.kt index 0098beee93..f6373e939f 100644 --- a/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfiguration.kt +++ b/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -24,7 +27,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class OnlineBankingPLConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -40,6 +43,22 @@ class OnlineBankingPLConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -47,14 +66,15 @@ class OnlineBankingPLConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -81,3 +101,38 @@ class OnlineBankingPLConfiguration private constructor( } } } + +fun CheckoutConfiguration.onlineBankingPL( + configuration: @CheckoutConfigurationMarker OnlineBankingPLConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = OnlineBankingPLConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ONLINE_BANKING_PL, config) + return this +} + +fun CheckoutConfiguration.getOnlineBankingPLConfiguration(): OnlineBankingPLConfiguration? { + return getConfiguration(PaymentMethodTypes.ONLINE_BANKING_PL) +} + +internal fun OnlineBankingPLConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ONLINE_BANKING_PL, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/internal/provider/OnlineBankingPLComponentProvider.kt b/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/internal/provider/OnlineBankingPLComponentProvider.kt index 1fde1619f6..207acfc642 100644 --- a/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/internal/provider/OnlineBankingPLComponentProvider.kt +++ b/online-banking-pl/src/main/java/com/adyen/checkout/onlinebankingpl/internal/provider/OnlineBankingPLComponentProvider.kt @@ -11,23 +11,24 @@ package com.adyen.checkout.onlinebankingpl.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.OnlineBankingPLPaymentMethod import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.onlinebankingpl.OnlineBankingPLComponent import com.adyen.checkout.onlinebankingpl.OnlineBankingPLComponentState import com.adyen.checkout.onlinebankingpl.OnlineBankingPLConfiguration +import com.adyen.checkout.onlinebankingpl.getOnlineBankingPLConfiguration +import com.adyen.checkout.onlinebankingpl.toCheckoutConfiguration class OnlineBankingPLComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider< OnlineBankingPLComponent, @@ -36,8 +37,7 @@ constructor( OnlineBankingPLComponentState >( componentClass = OnlineBankingPLComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -62,4 +62,12 @@ constructor( override fun createPaymentMethod() = OnlineBankingPLPaymentMethod() override fun getSupportedPaymentMethods(): List = OnlineBankingPLComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): OnlineBankingPLConfiguration? { + return checkoutConfiguration.getOnlineBankingPLConfiguration() + } + + override fun getCheckoutConfiguration(configuration: OnlineBankingPLConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/online-banking-pl/src/test/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfigurationTest.kt b/online-banking-pl/src/test/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfigurationTest.kt new file mode 100644 index 0000000000..3ec1d4f0d0 --- /dev/null +++ b/online-banking-pl/src/test/java/com/adyen/checkout/onlinebankingpl/OnlineBankingPLConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.onlinebankingpl + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class OnlineBankingPLConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + onlineBankingPL { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getOnlineBankingPLConfiguration() + + val expected = OnlineBankingPLConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = OnlineBankingPLConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualPlConfig = actual.getOnlineBankingPLConfiguration() + assertEquals(config.shopperLocale, actualPlConfig?.shopperLocale) + assertEquals(config.environment, actualPlConfig?.environment) + assertEquals(config.clientKey, actualPlConfig?.clientKey) + assertEquals(config.amount, actualPlConfig?.amount) + assertEquals(config.analyticsConfiguration, actualPlConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualPlConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualPlConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualPlConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/online-banking-sk/build.gradle b/online-banking-sk/build.gradle index e1d3111f6d..636107cfb4 100644 --- a/online-banking-sk/build.gradle +++ b/online-banking-sk/build.gradle @@ -37,4 +37,7 @@ dependencies { // Checkout api project(':action-core') api project(':online-banking-core') + + testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito } diff --git a/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfiguration.kt b/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfiguration.kt index 909f622c41..736fbccbf7 100644 --- a/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfiguration.kt +++ b/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.onlinebankingcore.internal.OnlineBankingConfiguration import kotlinx.parcelize.Parcelize @@ -23,7 +26,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class OnlineBankingSKConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -37,6 +40,22 @@ class OnlineBankingSKConfiguration private constructor( */ class Builder : OnlineBankingConfigurationBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,14 +63,15 @@ class OnlineBankingSKConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -76,3 +96,38 @@ class OnlineBankingSKConfiguration private constructor( } } } + +fun CheckoutConfiguration.onlineBankingSK( + configuration: @CheckoutConfigurationMarker OnlineBankingSKConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = OnlineBankingSKConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ONLINE_BANKING_SK, config) + return this +} + +fun CheckoutConfiguration.getOnlineBankingSKConfiguration(): OnlineBankingSKConfiguration? { + return getConfiguration(PaymentMethodTypes.ONLINE_BANKING_SK) +} + +internal fun OnlineBankingSKConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ONLINE_BANKING_SK, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/internal/provider/OnlineBankingSKComponentProvider.kt b/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/internal/provider/OnlineBankingSKComponentProvider.kt index 37a8a2b77d..69dc9425aa 100644 --- a/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/internal/provider/OnlineBankingSKComponentProvider.kt +++ b/online-banking-sk/src/main/java/com/adyen/checkout/onlinebankingsk/internal/provider/OnlineBankingSKComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.onlinebankingsk.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.OnlineBankingSKPaymentMethod import com.adyen.checkout.onlinebankingcore.internal.provider.OnlineBankingComponentProvider import com.adyen.checkout.onlinebankingcore.internal.ui.OnlineBankingDelegate import com.adyen.checkout.onlinebankingsk.OnlineBankingSKComponent import com.adyen.checkout.onlinebankingsk.OnlineBankingSKComponentState import com.adyen.checkout.onlinebankingsk.OnlineBankingSKConfiguration +import com.adyen.checkout.onlinebankingsk.getOnlineBankingSKConfiguration +import com.adyen.checkout.onlinebankingsk.toCheckoutConfiguration class OnlineBankingSKComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : OnlineBankingComponentProvider< OnlineBankingSKComponent, OnlineBankingSKConfiguration, OnlineBankingSKPaymentMethod, - OnlineBankingSKComponentState + OnlineBankingSKComponentState, >( componentClass = OnlineBankingSKComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -69,7 +69,15 @@ constructor( delegate = delegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = actionHandlingComponent, - componentEventHandler = componentEventHandler + componentEventHandler = componentEventHandler, ) } + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): OnlineBankingSKConfiguration? { + return checkoutConfiguration.getOnlineBankingSKConfiguration() + } + + override fun getCheckoutConfiguration(configuration: OnlineBankingSKConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/online-banking-sk/src/test/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfigurationTest.kt b/online-banking-sk/src/test/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfigurationTest.kt new file mode 100644 index 0000000000..c4aea0ff0d --- /dev/null +++ b/online-banking-sk/src/test/java/com/adyen/checkout/onlinebankingsk/OnlineBankingSKConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.onlinebankingsk + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class OnlineBankingSKConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + onlineBankingSK { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getOnlineBankingSKConfiguration() + + val expected = OnlineBankingSKConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = OnlineBankingSKConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualSkConfig = actual.getOnlineBankingSKConfiguration() + assertEquals(config.shopperLocale, actualSkConfig?.shopperLocale) + assertEquals(config.environment, actualSkConfig?.environment) + assertEquals(config.clientKey, actualSkConfig?.clientKey) + assertEquals(config.amount, actualSkConfig?.amount) + assertEquals(config.analyticsConfiguration, actualSkConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualSkConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/openbanking/src/main/java/com/adyen/checkout/openbanking/OpenBankingConfiguration.kt b/openbanking/src/main/java/com/adyen/checkout/openbanking/OpenBankingConfiguration.kt index 8aa1f2338b..0faf10a278 100644 --- a/openbanking/src/main/java/com/adyen/checkout/openbanking/OpenBankingConfiguration.kt +++ b/openbanking/src/main/java/com/adyen/checkout/openbanking/OpenBankingConfiguration.kt @@ -11,6 +11,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.issuerlist.IssuerListViewType import com.adyen.checkout.issuerlist.internal.IssuerListConfiguration @@ -23,7 +26,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class OpenBankingConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -39,6 +42,22 @@ class OpenBankingConfiguration private constructor( */ class Builder : IssuerListBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -46,14 +65,15 @@ class OpenBankingConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -80,3 +100,38 @@ class OpenBankingConfiguration private constructor( } } } + +fun CheckoutConfiguration.openBanking( + configuration: @CheckoutConfigurationMarker OpenBankingConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = OpenBankingConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.OPEN_BANKING, config) + return this +} + +fun CheckoutConfiguration.getOpenBankingConfiguration(): OpenBankingConfiguration? { + return getConfiguration(PaymentMethodTypes.OPEN_BANKING) +} + +internal fun OpenBankingConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.OPEN_BANKING, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/openbanking/src/main/java/com/adyen/checkout/openbanking/internal/provider/OpenBankingComponentProvider.kt b/openbanking/src/main/java/com/adyen/checkout/openbanking/internal/provider/OpenBankingComponentProvider.kt index 2cd53fcf1c..c0e49f8f40 100644 --- a/openbanking/src/main/java/com/adyen/checkout/openbanking/internal/provider/OpenBankingComponentProvider.kt +++ b/openbanking/src/main/java/com/adyen/checkout/openbanking/internal/provider/OpenBankingComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.openbanking.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.OpenBankingPaymentMethod import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate import com.adyen.checkout.openbanking.OpenBankingComponent import com.adyen.checkout.openbanking.OpenBankingComponentState import com.adyen.checkout.openbanking.OpenBankingConfiguration +import com.adyen.checkout.openbanking.getOpenBankingConfiguration +import com.adyen.checkout.openbanking.toCheckoutConfiguration class OpenBankingComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : IssuerListComponentProvider< OpenBankingComponent, OpenBankingConfiguration, OpenBankingPaymentMethod, - OpenBankingComponentState + OpenBankingComponentState, >( componentClass = OpenBankingComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -62,4 +62,12 @@ constructor( override fun createPaymentMethod() = OpenBankingPaymentMethod() override fun getSupportedPaymentMethods(): List = OpenBankingComponent.PAYMENT_METHOD_TYPES + + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): OpenBankingConfiguration? { + return checkoutConfiguration.getOpenBankingConfiguration() + } + + override fun getCheckoutConfiguration(configuration: OpenBankingConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } } diff --git a/openbanking/src/test/java/com/adyen/checkout/openbanking/OpenBankingConfigurationTest.kt b/openbanking/src/test/java/com/adyen/checkout/openbanking/OpenBankingConfigurationTest.kt new file mode 100644 index 0000000000..0bfef1746c --- /dev/null +++ b/openbanking/src/test/java/com/adyen/checkout/openbanking/OpenBankingConfigurationTest.kt @@ -0,0 +1,99 @@ +package com.adyen.checkout.openbanking + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import com.adyen.checkout.issuerlist.IssuerListViewType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class OpenBankingConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + openBanking { + setViewType(IssuerListViewType.SPINNER_VIEW) + setHideIssuerLogos(true) + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getOpenBankingConfiguration() + + val expected = OpenBankingConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.viewType, actual?.viewType) + assertEquals(expected.hideIssuerLogos, actual?.hideIssuerLogos) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = OpenBankingConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setViewType(IssuerListViewType.SPINNER_VIEW) + .setHideIssuerLogos(true) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualOpenBankingConfig = actual.getOpenBankingConfiguration() + assertEquals(config.shopperLocale, actualOpenBankingConfig?.shopperLocale) + assertEquals(config.environment, actualOpenBankingConfig?.environment) + assertEquals(config.clientKey, actualOpenBankingConfig?.clientKey) + assertEquals(config.amount, actualOpenBankingConfig?.amount) + assertEquals(config.analyticsConfiguration, actualOpenBankingConfig?.analyticsConfiguration) + assertEquals(config.viewType, actualOpenBankingConfig?.viewType) + assertEquals(config.hideIssuerLogos, actualOpenBankingConfig?.hideIssuerLogos) + assertEquals(config.isSubmitButtonVisible, actualOpenBankingConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankComponent.kt b/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankComponent.kt index 7e5df52ceb..ff7aac748b 100644 --- a/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankComponent.kt +++ b/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankComponent.kt @@ -20,8 +20,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.paybybank.internal.provider.PayByBankComponentProvider import com.adyen.checkout.paybybank.internal.ui.PayByBankDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -71,19 +71,18 @@ class PayByBankComponent internal constructor( override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? PayByBankDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } payByBankDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = PayByBankComponentProvider() diff --git a/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankConfiguration.kt b/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankConfiguration.kt index 45605fe515..d37b1d9a8b 100644 --- a/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankConfiguration.kt +++ b/paybybank/src/main/java/com/adyen/checkout/paybybank/PayByBankConfiguration.kt @@ -13,7 +13,10 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -23,7 +26,7 @@ import java.util.Locale */ @Parcelize class PayByBankConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -35,6 +38,23 @@ class PayByBankConfiguration private constructor( * Builder to create a [PayByBankConfiguration]. */ class Builder : ActionHandlingPaymentMethodConfigurationBuilder { + + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -42,14 +62,15 @@ class PayByBankConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -73,3 +94,38 @@ class PayByBankConfiguration private constructor( } } } + +fun CheckoutConfiguration.payByBank( + configuration: @CheckoutConfigurationMarker PayByBankConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = PayByBankConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.PAY_BY_BANK, config) + return this +} + +fun CheckoutConfiguration.getPayByBankConfiguration(): PayByBankConfiguration? { + return getConfiguration(PaymentMethodTypes.PAY_BY_BANK) +} + +internal fun PayByBankConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.PAY_BY_BANK, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/provider/PayByBankComponentProvider.kt b/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/provider/PayByBankComponentProvider.kt index 0aa4b3d732..ab01e7ffa8 100644 --- a/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/provider/PayByBankComponentProvider.kt +++ b/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/provider/PayByBankComponentProvider.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -27,17 +28,19 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepositoryD import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.paybybank.PayByBankComponent import com.adyen.checkout.paybybank.PayByBankComponentState import com.adyen.checkout.paybybank.PayByBankConfiguration import com.adyen.checkout.paybybank.internal.ui.DefaultPayByBankDelegate +import com.adyen.checkout.paybybank.toCheckoutConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -52,31 +55,29 @@ import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class PayByBankComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< PayByBankComponent, PayByBankConfiguration, PayByBankComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< PayByBankComponent, PayByBankConfiguration, PayByBankComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = GenericComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: PayByBankConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -85,7 +86,12 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -94,7 +100,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -108,8 +114,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -118,7 +124,7 @@ constructor( payByBankDelegate = payByBankDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, payByBankDelegate), - componentEventHandler = DefaultComponentEventHandler() + componentEventHandler = DefaultComponentEventHandler(), ) } @@ -130,6 +136,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: PayByBankConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): PayByBankComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -137,7 +167,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: PayByBankConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -145,10 +175,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -159,7 +192,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -173,8 +206,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -189,7 +222,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -200,7 +233,7 @@ constructor( payByBankDelegate = payByBankDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, payByBankDelegate), - componentEventHandler = sessionComponentEventHandler + componentEventHandler = sessionComponentEventHandler, ) } @@ -212,6 +245,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: PayByBankConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): PayByBankComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegate.kt b/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegate.kt index 1ffc5c20ca..f199dc34c8 100644 --- a/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegate.kt +++ b/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegate.kt @@ -21,8 +21,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.paymentmethod.PayByBankPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.issuerlist.internal.ui.model.IssuerModel import com.adyen.checkout.paybybank.PayByBankComponentState import com.adyen.checkout.paybybank.internal.ui.model.PayByBankInputData @@ -80,7 +80,7 @@ internal class DefaultPayByBankDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -97,7 +97,7 @@ internal class DefaultPayByBankDelegate( submitFlow = submitFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -121,7 +121,7 @@ internal class DefaultPayByBankDelegate( private fun createOutputData() = PayByBankOutputData( selectedIssuer = inputData.selectedIssuer, - issuers = filterByQuery() + issuers = filterByQuery(), ) private fun filterByQuery(): List = inputData.query?.let { query -> @@ -157,7 +157,7 @@ internal class DefaultPayByBankDelegate( return PayByBankComponentState( data = paymentComponentData, isInputValid = outputData?.isValid ?: true, - isReady = true + isReady = true, ) } @@ -197,8 +197,4 @@ internal class DefaultPayByBankDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/view/PayByBankView.kt b/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/view/PayByBankView.kt index 70cd693506..9551fe7a4c 100644 --- a/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/view/PayByBankView.kt +++ b/paybybank/src/main/java/com/adyen/checkout/paybybank/internal/ui/view/PayByBankView.kt @@ -15,8 +15,8 @@ import android.view.View import android.widget.LinearLayout import androidx.core.view.isVisible import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.issuerlist.internal.ui.model.IssuerModel import com.adyen.checkout.paybybank.R import com.adyen.checkout.paybybank.databinding.PayByBankViewBinding @@ -76,16 +76,16 @@ internal class PayByBankView @JvmOverloads constructor( private fun initLocalizedStrings(localizedContext: Context) { binding.textInputLayoutSearchQuery.setLocalizedHintFromStyle( R.style.AdyenCheckout_PayByBank_SearchQueryInput, - localizedContext + localizedContext, ) binding.textViewNoMatchingIssuers.setLocalizedTextFromStyle( R.style.AdyenCheckout_PayByBank_NoMatchingIssuers, - localizedContext + localizedContext, ) } private fun onItemClicked(issuerModel: IssuerModel) { - Logger.d(TAG, "onItemClicked - ${issuerModel.name}") + adyenLog(AdyenLogLevel.DEBUG) { "onItemClicked - ${issuerModel.name}" } delegate.updateInputData { selectedIssuer = issuerModel } delegate.onSubmit() } @@ -99,7 +99,7 @@ internal class PayByBankView @JvmOverloads constructor( private fun initIssuersRecyclerView() { payByBankRecyclerAdapter = PayByBankRecyclerAdapter( paymentMethod = delegate.getPaymentMethodType(), - onItemClicked = ::onItemClicked + onItemClicked = ::onItemClicked, ).apply { submitList(delegate.getIssuers()) } @@ -111,8 +111,4 @@ internal class PayByBankView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankComponentTest.kt b/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankComponentTest.kt index 4df80690ff..e9bdcd2560 100644 --- a/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankComponentTest.kt +++ b/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankComponentTest.kt @@ -15,10 +15,9 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.paybybank.internal.ui.PayByBankComponentViewType import com.adyen.checkout.paybybank.internal.ui.PayByBankDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -39,7 +38,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class PayByBankComponentTest( @Mock private val payByBankDelegate: PayByBankDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -58,9 +57,8 @@ internal class PayByBankComponentTest( payByBankDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -115,7 +113,7 @@ internal class PayByBankComponentTest( payByBankDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) component.viewFlow.test { diff --git a/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankConfigurationTest.kt b/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankConfigurationTest.kt new file mode 100644 index 0000000000..5ef2c7da34 --- /dev/null +++ b/paybybank/src/test/java/com/adyen/checkout/paybybank/PayByBankConfigurationTest.kt @@ -0,0 +1,82 @@ +package com.adyen.checkout.paybybank + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class PayByBankConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + payByBank() + } + + val actual = checkoutConfiguration.getPayByBankConfiguration() + + val expected = PayByBankConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = PayByBankConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualPayConfig = actual.getPayByBankConfiguration() + assertEquals(config.shopperLocale, actualPayConfig?.shopperLocale) + assertEquals(config.environment, actualPayConfig?.environment) + assertEquals(config.clientKey, actualPayConfig?.clientKey) + assertEquals(config.amount, actualPayConfig?.amount) + assertEquals(config.analyticsConfiguration, actualPayConfig?.analyticsConfiguration) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/paybybank/src/test/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegateTest.kt b/paybybank/src/test/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegateTest.kt index 208d2c2bea..7a3d149cba 100644 --- a/paybybank/src/test/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegateTest.kt +++ b/paybybank/src/test/java/com/adyen/checkout/paybybank/internal/ui/DefaultPayByBankDelegateTest.kt @@ -10,20 +10,21 @@ package com.adyen.checkout.paybybank.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Issuer import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.issuerlist.internal.ui.model.IssuerModel import com.adyen.checkout.paybybank.PayByBankComponentState -import com.adyen.checkout.paybybank.PayByBankConfiguration import com.adyen.checkout.paybybank.internal.ui.model.PayByBankOutputData +import com.adyen.checkout.paybybank.payByBank +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,10 +47,10 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.kotlin.doReturn import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import java.util.* +import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultPayByBankDelegateTest( @Mock private val analyticsRepository: AnalyticsRepository, @Mock private val submitHandler: SubmitHandler, @@ -61,10 +62,9 @@ internal class DefaultPayByBankDelegateTest( fun beforeEach() { delegate = createPayByBankDelegate( issuers = listOf( - Issuer(id = "issuer-id", name = "issuer-name") - ) + Issuer(id = "issuer-id", name = "issuer-name"), + ), ) - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -132,8 +132,8 @@ internal class DefaultPayByBankDelegateTest( delegate.updateComponentState( PayByBankOutputData( issuer, - listOf(issuer) - ) + listOf(issuer), + ), ) with(expectMostRecentItem()) { assertEquals("issuer-id", data.paymentMethod?.issuer) @@ -146,10 +146,8 @@ internal class DefaultPayByBankDelegateTest( @Test fun `when issuers is empty, then component state should be valid`() = runTest { - val configuration = getPayByBankConfigurationBuilder().build() delegate = createPayByBankDelegate( issuers = emptyList(), - configuration = configuration, ) delegate.componentStateFlow.test { with(expectMostRecentItem()) { @@ -167,13 +165,7 @@ internal class DefaultPayByBankDelegateTest( configurationValue: Amount?, expectedComponentStateValue: Amount?, ) = runTest { - val configuration = getPayByBankConfigurationBuilder() - .apply { - configurationValue?.let { - setAmount(it) - } - } - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createPayByBankDelegate( issuers = listOf(Issuer(id = "issuer-id", name = "issuer-name")), configuration = configuration, @@ -184,8 +176,8 @@ internal class DefaultPayByBankDelegateTest( delegate.updateComponentState( PayByBankOutputData( issuer, - listOf(issuer) - ) + listOf(issuer), + ), ) assertEquals(expectedComponentStateValue, expectMostRecentItem().data.amount) } @@ -195,7 +187,7 @@ internal class DefaultPayByBankDelegateTest( @Test fun `when issuers is empty in paymentMethod then viewFlow should emit null`() = runTest { delegate = createPayByBankDelegate( - issuers = emptyList() + issuers = emptyList(), ) delegate.viewFlow.test { assertEquals(null, expectMostRecentItem()) @@ -206,8 +198,8 @@ internal class DefaultPayByBankDelegateTest( fun `when issuers is not empty in paymentMethod then viewFlow should emit PayByBankComponentViewType`() = runTest { delegate = createPayByBankDelegate( issuers = listOf( - Issuer(id = "issuer-id", name = "issuer-name") - ) + Issuer(id = "issuer-id", name = "issuer-name"), + ), ) delegate.viewFlow.test { assertEquals(PayByBankComponentViewType, expectMostRecentItem()) @@ -276,24 +268,28 @@ internal class DefaultPayByBankDelegateTest( } } - private fun getPayByBankConfigurationBuilder(): PayByBankConfiguration.Builder { - return PayByBankConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + payByBank() } private fun createPayByBankDelegate( issuers: List, order: Order? = TEST_ORDER, - configuration: PayByBankConfiguration = getPayByBankConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), ): DefaultPayByBankDelegate { return DefaultPayByBankDelegate( observerRepository = PaymentObserverRepository(), - componentParams = GenericComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), paymentMethod = PaymentMethod( - issuers = issuers + issuers = issuers, ), order = order, analyticsRepository = analyticsRepository, diff --git a/payeasy/build.gradle b/payeasy/build.gradle index d28afe0e82..96198eb90f 100644 --- a/payeasy/build.gradle +++ b/payeasy/build.gradle @@ -32,4 +32,7 @@ android { dependencies { api project(':econtext') + + testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito } diff --git a/payeasy/src/main/java/com/adyen/checkout/payeasy/PayEasyConfiguration.kt b/payeasy/src/main/java/com/adyen/checkout/payeasy/PayEasyConfiguration.kt index 6be74bf957..5832e37dcd 100644 --- a/payeasy/src/main/java/com/adyen/checkout/payeasy/PayEasyConfiguration.kt +++ b/payeasy/src/main/java/com/adyen/checkout/payeasy/PayEasyConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.econtext.internal.EContextConfiguration import kotlinx.parcelize.Parcelize @@ -23,7 +26,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class PayEasyConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -37,6 +40,22 @@ class PayEasyConfiguration private constructor( */ class Builder : EContextConfiguration.Builder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,14 +63,15 @@ class PayEasyConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -76,3 +96,38 @@ class PayEasyConfiguration private constructor( } } } + +fun CheckoutConfiguration.payEasy( + configuration: @CheckoutConfigurationMarker PayEasyConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = PayEasyConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ECONTEXT_ATM, config) + return this +} + +fun CheckoutConfiguration.getPayEasyConfiguration(): PayEasyConfiguration? { + return getConfiguration(PaymentMethodTypes.ECONTEXT_ATM) +} + +internal fun PayEasyConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ECONTEXT_ATM, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/payeasy/src/main/java/com/adyen/checkout/payeasy/internal/provider/PayEasyComponentProvider.kt b/payeasy/src/main/java/com/adyen/checkout/payeasy/internal/provider/PayEasyComponentProvider.kt index 6f162c8b9e..7b4839a19b 100644 --- a/payeasy/src/main/java/com/adyen/checkout/payeasy/internal/provider/PayEasyComponentProvider.kt +++ b/payeasy/src/main/java/com/adyen/checkout/payeasy/internal/provider/PayEasyComponentProvider.kt @@ -11,28 +11,28 @@ package com.adyen.checkout.payeasy.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.PayEasyPaymentMethod import com.adyen.checkout.econtext.internal.provider.EContextComponentProvider import com.adyen.checkout.econtext.internal.ui.EContextDelegate import com.adyen.checkout.payeasy.PayEasyComponent import com.adyen.checkout.payeasy.PayEasyComponentState import com.adyen.checkout.payeasy.PayEasyConfiguration +import com.adyen.checkout.payeasy.getPayEasyConfiguration +import com.adyen.checkout.payeasy.toCheckoutConfiguration class PayEasyComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : EContextComponentProvider( componentClass = PayEasyComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -60,6 +60,14 @@ constructor( return PayEasyPaymentMethod() } + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): PayEasyConfiguration? { + return checkoutConfiguration.getPayEasyConfiguration() + } + + override fun getCheckoutConfiguration(configuration: PayEasyConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } + override fun getSupportedPaymentMethods(): List { return PayEasyComponent.PAYMENT_METHOD_TYPES } diff --git a/payeasy/src/test/java/com/adyen/checkout/payeasy/PayEasyConfigurationTest.kt b/payeasy/src/test/java/com/adyen/checkout/payeasy/PayEasyConfigurationTest.kt new file mode 100644 index 0000000000..055120f4e5 --- /dev/null +++ b/payeasy/src/test/java/com/adyen/checkout/payeasy/PayEasyConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.payeasy + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class PayEasyConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + payEasy { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getPayEasyConfiguration() + + val expected = PayEasyConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = PayEasyConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualPayEasyConfig = actual.getPayEasyConfiguration() + assertEquals(config.shopperLocale, actualPayEasyConfig?.shopperLocale) + assertEquals(config.environment, actualPayEasyConfig?.environment) + assertEquals(config.clientKey, actualPayEasyConfig?.clientKey) + assertEquals(config.amount, actualPayEasyConfig?.amount) + assertEquals(config.analyticsConfiguration, actualPayEasyConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualPayEasyConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeComponent.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeComponent.kt index ccb02db422..3647075b43 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeComponent.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeComponent.kt @@ -19,8 +19,8 @@ import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler import com.adyen.checkout.components.core.internal.IntentHandlingComponent import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.qrcode.internal.provider.QRCodeComponentProvider import com.adyen.checkout.qrcode.internal.ui.QRCodeDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -77,12 +77,11 @@ class QRCodeComponent internal constructor( override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } delegate.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER: ActionComponentProvider = diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeConfiguration.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeConfiguration.kt index 7648eb2ba1..4cf49d0c4f 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeConfiguration.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/QRCodeConfiguration.kt @@ -10,8 +10,10 @@ package com.adyen.checkout.qrcode import android.content.Context import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -21,7 +23,7 @@ import java.util.Locale */ @Parcelize class QRCodeConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -33,6 +35,22 @@ class QRCodeConfiguration private constructor( */ class Builder : BaseConfigurationBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -40,14 +58,15 @@ class QRCodeConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -56,7 +75,7 @@ class QRCodeConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) override fun buildInternal(): QRCodeConfiguration { @@ -70,3 +89,34 @@ class QRCodeConfiguration private constructor( } } } + +fun CheckoutConfiguration.qrCode( + configuration: @CheckoutConfigurationMarker QRCodeConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = QRCodeConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addActionConfiguration(config) + return this +} + +fun CheckoutConfiguration.getQRCodeConfiguration(): QRCodeConfiguration? { + return getActionConfiguration(QRCodeConfiguration::class.java) +} + +internal fun QRCodeConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addActionConfiguration(this@toCheckoutConfiguration) + } +} diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/provider/QRCodeComponentProvider.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/provider/QRCodeComponentProvider.kt index 7ef691a030..787954eef8 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/provider/QRCodeComponentProvider.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/provider/QRCodeComponentProvider.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.QrCodeAction import com.adyen.checkout.components.core.internal.ActionObserverRepository @@ -24,43 +25,43 @@ import com.adyen.checkout.components.core.internal.PaymentDataRepository import com.adyen.checkout.components.core.internal.data.api.DefaultStatusRepository import com.adyen.checkout.components.core.internal.data.api.StatusService import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.internal.data.api.HttpClientFactory -import com.adyen.checkout.core.internal.util.FileDownloader +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.qrcode.QRCodeComponent import com.adyen.checkout.qrcode.QRCodeConfiguration import com.adyen.checkout.qrcode.internal.QRCodeCountDownTimer import com.adyen.checkout.qrcode.internal.ui.DefaultQRCodeDelegate import com.adyen.checkout.qrcode.internal.ui.QRCodeDelegate +import com.adyen.checkout.qrcode.toCheckoutConfiguration import com.adyen.checkout.ui.core.internal.DefaultRedirectHandler +import com.adyen.checkout.ui.core.internal.util.ImageSaver class QRCodeComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : ActionComponentProvider { - private val componentParamsMapper = GenericComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, application: Application, - configuration: QRCodeConfiguration, + checkoutConfiguration: CheckoutConfiguration, callback: ActionComponentCallback, - key: String?, + key: String? ): QRCodeComponent { val qrCodeFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val qrCodeDelegate = getDelegate(configuration, savedStateHandle, application) + val qrCodeDelegate = getDelegate(checkoutConfiguration, savedStateHandle, application) QRCodeComponent( delegate = qrCodeDelegate, - actionComponentEventHandler = DefaultActionComponentEventHandler(callback) + actionComponentEventHandler = DefaultActionComponentEventHandler(callback), ) } return ViewModelProvider(viewModelStoreOwner, qrCodeFactory)[key, QRCodeComponent::class.java] @@ -70,14 +71,20 @@ constructor( } override fun getDelegate( - configuration: QRCodeConfiguration, + checkoutConfiguration: CheckoutConfiguration, savedStateHandle: SavedStateHandle, - application: Application, + application: Application ): QRCodeDelegate { - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val statusService = StatusService(httpClient) - val statusRepository = DefaultStatusRepository(statusService, configuration.clientKey) + val statusRepository = DefaultStatusRepository(statusService, componentParams.clientKey) val countDownTimer = QRCodeCountDownTimer() val redirectHandler = DefaultRedirectHandler() val paymentDataRepository = PaymentDataRepository(savedStateHandle) @@ -89,7 +96,27 @@ constructor( statusCountDownTimer = countDownTimer, redirectHandler = redirectHandler, paymentDataRepository = paymentDataRepository, - fileDownloader = FileDownloader(application) + imageSaver = ImageSaver(), + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + configuration: QRCodeConfiguration, + callback: ActionComponentCallback, + key: String?, + ): QRCodeComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + application = application, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + key = key, ) } diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegate.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegate.kt index d3e7ca67c8..0eac440161 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegate.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegate.kt @@ -8,11 +8,10 @@ package com.adyen.checkout.qrcode.internal.ui -import android.Manifest import android.app.Activity +import android.content.Context import android.content.Intent import android.net.Uri -import androidx.annotation.RequiresPermission import androidx.annotation.VisibleForTesting import androidx.lifecycle.LifecycleOwner import com.adyen.checkout.components.core.ActionComponentData @@ -22,24 +21,28 @@ import com.adyen.checkout.components.core.action.QrCodeAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.PermissionRequestData import com.adyen.checkout.components.core.internal.data.api.StatusRepository import com.adyen.checkout.components.core.internal.data.model.StatusResponse import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.internal.ui.model.TimerData +import com.adyen.checkout.components.core.internal.util.DateUtils import com.adyen.checkout.components.core.internal.util.StatusResponseUtils import com.adyen.checkout.components.core.internal.util.bufferedChannel import com.adyen.checkout.components.core.internal.util.repeatOnResume +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.FileDownloader -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.qrcode.internal.QRCodeCountDownTimer import com.adyen.checkout.qrcode.internal.ui.model.QRCodeOutputData import com.adyen.checkout.qrcode.internal.ui.model.QRCodePaymentMethodConfig import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent import com.adyen.checkout.ui.core.internal.RedirectHandler +import com.adyen.checkout.ui.core.internal.exception.PermissionRequestException import com.adyen.checkout.ui.core.internal.ui.ComponentViewType +import com.adyen.checkout.ui.core.internal.util.ImageSaver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -51,6 +54,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import org.json.JSONException import org.json.JSONObject +import java.util.Calendar import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -62,17 +66,20 @@ internal class DefaultQRCodeDelegate( private val statusCountDownTimer: QRCodeCountDownTimer, private val redirectHandler: RedirectHandler, private val paymentDataRepository: PaymentDataRepository, - private val fileDownloader: FileDownloader + private val imageSaver: ImageSaver ) : QRCodeDelegate { private val _outputDataFlow = MutableStateFlow(createOutputData()) override val outputDataFlow: Flow = _outputDataFlow - override val outputData: QRCodeOutputData get() = _outputDataFlow.value - private val exceptionChannel: Channel = bufferedChannel() override val exceptionFlow: Flow = exceptionChannel.receiveAsFlow() + private val permissionChannel: Channel = bufferedChannel() + override val permissionFlow: Flow = permissionChannel.receiveAsFlow() + + override val outputData: QRCodeOutputData get() = _outputDataFlow.value + private val detailsChannel: Channel = bufferedChannel() override val detailsFlow: Flow = detailsChannel.receiveAsFlow() @@ -95,7 +102,7 @@ internal class DefaultQRCodeDelegate( private fun attachStatusTimer() { statusCountDownTimer.attach( millisInFuture = maxPollingDurationMillis, - countDownInterval = STATUS_POLLING_INTERVAL_MILLIS + countDownInterval = STATUS_POLLING_INTERVAL_MILLIS, ) { millisUntilFinished -> onTimerTick(millisUntilFinished) } } @@ -118,9 +125,10 @@ internal class DefaultQRCodeDelegate( observerRepository.addObservers( detailsFlow = detailsFlow, exceptionFlow = exceptionFlow, + permissionFlow = permissionFlow, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) // Immediately request a new status if the user resumes the app @@ -141,13 +149,13 @@ internal class DefaultQRCodeDelegate( val paymentData = action.paymentData paymentDataRepository.paymentData = paymentData if (paymentData == null) { - Logger.e(TAG, "Payment data is null") + adyenLog(AdyenLogLevel.ERROR) { "Payment data is null" } exceptionChannel.trySend(ComponentException("Payment data is null")) return } if (shouldLaunchRedirect(action)) { - Logger.d(TAG, "Action does not require a view, redirecting.") + adyenLog(AdyenLogLevel.DEBUG) { "Action does not require a view, redirecting." } _viewFlow.tryEmit(QrCodeComponentViewType.REDIRECT) makeRedirect(activity, action) return @@ -173,7 +181,7 @@ internal class DefaultQRCodeDelegate( private fun makeRedirect(activity: Activity, action: QrCodeAction) { val url = action.url try { - Logger.d(TAG, "makeRedirect - $url") + adyenLog(AdyenLogLevel.DEBUG) { "makeRedirect - $url" } redirectHandler.launchUriRedirect(activity, url) } catch (ex: CheckoutException) { exceptionChannel.trySend(ex) @@ -190,16 +198,16 @@ internal class DefaultQRCodeDelegate( private fun onStatus(result: Result, action: QrCodeAction) { result.fold( onSuccess = { response -> - Logger.v(TAG, "Status changed - ${response.resultCode}") + adyenLog(AdyenLogLevel.VERBOSE) { "Status changed - ${response.resultCode}" } createOutputData(response, action) if (StatusResponseUtils.isFinalResult(response)) { onPollingSuccessful(response) } }, onFailure = { - Logger.e(TAG, "Error while polling status", it) + adyenLog(AdyenLogLevel.ERROR, it) { "Error while polling status" } exceptionChannel.trySend(ComponentException("Error while polling status", it)) - } + }, ) } @@ -212,7 +220,7 @@ internal class DefaultQRCodeDelegate( qrImageUrl = String.format( QR_IMAGE_BASE_PATH, componentParams.environment.checkoutShopperBaseUrl.toString(), - encodedQrCodeData + encodedQrCodeData, ) } @@ -227,7 +235,7 @@ internal class DefaultQRCodeDelegate( paymentMethodType = action.paymentMethodType, qrCodeData = action.qrCodeData, qrImageUrl = qrImageUrl, - messageTextResource = messageTextResource + messageTextResource = messageTextResource, ) _outputDataFlow.tryEmit(outputData) } @@ -285,27 +293,41 @@ internal class DefaultQRCodeDelegate( private fun createOutputData() = QRCodeOutputData( isValid = false, paymentMethodType = null, - qrCodeData = null + qrCodeData = null, ) - @RequiresPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - override fun downloadQRImage() { - val date: Long = System.currentTimeMillis() - val imageName = String.format(IMAGE_NAME_FORMAT, date) - val imageDirectory = android.os.Environment.DIRECTORY_DOWNLOADS.orEmpty() + override fun downloadQRImage(context: Context) { + val paymentMethodType = outputData.paymentMethodType ?: "" + val timestamp = DateUtils.formatDateToString(Calendar.getInstance()) + val imageName = String.format(IMAGE_NAME_FORMAT, paymentMethodType, timestamp) + coroutineScope.launch { - fileDownloader.download( - outputData.qrImageUrl.orEmpty(), - imageName, - imageDirectory, - MIME_TYPE + imageSaver.saveImageFromUrl( + context = context, + permissionHandler = this@DefaultQRCodeDelegate, + imageUrl = outputData.qrImageUrl.orEmpty(), + fileName = imageName, ).fold( - onSuccess = { eventChannel.trySend(QrCodeUIEvent.QrImageDownloadResult.Success) }, - onFailure = { e -> eventChannel.trySend(QrCodeUIEvent.QrImageDownloadResult.Failure(e)) } + onSuccess = { + eventChannel.trySend(QrCodeUIEvent.QrImageDownloadResult.Success) + }, + onFailure = { throwable -> + when (throwable) { + is PermissionRequestException -> + eventChannel.trySend(QrCodeUIEvent.QrImageDownloadResult.PermissionDenied) + + else -> eventChannel.trySend(QrCodeUIEvent.QrImageDownloadResult.Failure(throwable)) + } + }, ) } } + override fun requestPermission(context: Context, requiredPermission: String, callback: PermissionHandlerCallback) { + val requestData = PermissionRequestData(requiredPermission, callback) + permissionChannel.trySend(requestData) + } + override fun setOnRedirectListener(listener: () -> Unit) { redirectHandler.setOnRedirectListener(listener) } @@ -320,7 +342,6 @@ internal class DefaultQRCodeDelegate( } companion object { - private val TAG = LogUtil.getTag() private val VIEWABLE_PAYMENT_METHODS = listOf( PaymentMethodTypes.DUIT_NOW, @@ -336,8 +357,7 @@ internal class DefaultQRCodeDelegate( private val DEFAULT_MAX_POLLING_DURATION = 15.minutes.inWholeMilliseconds private const val HUNDRED = 100 - private const val IMAGE_NAME_FORMAT = "QR-code-%s.png" + private const val IMAGE_NAME_FORMAT = "%s-%s.png" private const val QR_IMAGE_BASE_PATH = "%sbarcode.shtml?barcodeType=qrCode&fileType=png&data=%s" - private const val MIME_TYPE = "image/png" } } diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QRCodeDelegate.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QRCodeDelegate.kt index 6cba238ab4..8a079c69f6 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QRCodeDelegate.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/QRCodeDelegate.kt @@ -8,13 +8,16 @@ package com.adyen.checkout.qrcode.internal.ui +import android.content.Context import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.internal.ui.ActionDelegate import com.adyen.checkout.components.core.internal.ui.DetailsEmittingDelegate import com.adyen.checkout.components.core.internal.ui.IntentHandlingDelegate +import com.adyen.checkout.components.core.internal.ui.PermissionRequestingDelegate import com.adyen.checkout.components.core.internal.ui.RedirectableDelegate import com.adyen.checkout.components.core.internal.ui.StatusPollingDelegate import com.adyen.checkout.components.core.internal.ui.ViewableDelegate +import com.adyen.checkout.core.internal.ui.PermissionHandler import com.adyen.checkout.qrcode.internal.ui.model.QRCodeOutputData import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate @@ -28,9 +31,11 @@ interface QRCodeDelegate : IntentHandlingDelegate, StatusPollingDelegate, ViewProvidingDelegate, - RedirectableDelegate { + RedirectableDelegate, + PermissionRequestingDelegate, + PermissionHandler { val eventFlow: Flow - fun downloadQRImage() + fun downloadQRImage(context: Context) } diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QRCodePaymentMethodConfig.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QRCodePaymentMethodConfig.kt index cff159f0b7..822dd544dd 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QRCodePaymentMethodConfig.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QRCodePaymentMethodConfig.kt @@ -54,7 +54,7 @@ internal enum class QRCodePaymentMethodConfig( companion object { fun getByPaymentMethodType(paymentMethodType: String): QRCodePaymentMethodConfig { - return values().firstOrNull { it.paymentMethodType == paymentMethodType } ?: DEFAULT + return entries.firstOrNull { it.paymentMethodType == paymentMethodType } ?: DEFAULT } } } diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QrCodeUIEvent.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QrCodeUIEvent.kt index e50877bd35..e37cf4e601 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QrCodeUIEvent.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/model/QrCodeUIEvent.kt @@ -13,7 +13,8 @@ import androidx.annotation.RestrictTo @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) sealed class QrCodeUIEvent { sealed class QrImageDownloadResult : QrCodeUIEvent() { - object Success : QrImageDownloadResult() + data object Success : QrImageDownloadResult() + data object PermissionDenied : QrImageDownloadResult() data class Failure(val throwable: Throwable) : QrImageDownloadResult() } } diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/FullQRCodeView.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/FullQRCodeView.kt index 21c3e09d11..9faf8f93ce 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/FullQRCodeView.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/FullQRCodeView.kt @@ -8,30 +8,28 @@ package com.adyen.checkout.qrcode.internal.ui.view -import android.Manifest import android.content.Context -import android.content.pm.PackageManager -import android.os.Build import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.annotation.StringRes -import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.components.core.internal.ui.model.TimerData import com.adyen.checkout.components.core.internal.util.CurrencyUtils import com.adyen.checkout.components.core.internal.util.toast -import com.adyen.checkout.core.exception.PermissionException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.qrcode.R import com.adyen.checkout.qrcode.databinding.FullQrcodeViewBinding import com.adyen.checkout.qrcode.internal.ui.QRCodeDelegate import com.adyen.checkout.qrcode.internal.ui.model.QRCodeOutputData import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent +import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent.QrImageDownloadResult.Failure +import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent.QrImageDownloadResult.PermissionDenied +import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent.QrImageDownloadResult.Success import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.ui.LogoSize import com.adyen.checkout.ui.core.internal.ui.load @@ -71,21 +69,7 @@ internal class FullQRCodeView @JvmOverloads constructor( observeDelegate(delegate, coroutineScope) - binding.buttonSaveImage.setOnClickListener { - val requiredPermission = Manifest.permission.WRITE_EXTERNAL_STORAGE - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && - ContextCompat.checkSelfPermission(context, requiredPermission) != PackageManager.PERMISSION_GRANTED - ) { - delegate.onError( - PermissionException( - errorMessage = "$requiredPermission permission is not granted", - requiredPermission = requiredPermission - ) - ) - return@setOnClickListener - } - delegate.downloadQRImage() - } + binding.buttonSaveImage.setOnClickListener { delegate.downloadQRImage(context) } } private fun initLocalizedStrings(localizedContext: Context) { @@ -107,7 +91,7 @@ internal class FullQRCodeView @JvmOverloads constructor( } private fun outputDataChanged(outputData: QRCodeOutputData) { - Logger.d(TAG, "outputDataChanged") + adyenLog(AdyenLogLevel.DEBUG) { "outputDataChanged" } updateMessageText(outputData.messageTextResource) updateLogo(outputData.paymentMethodType) @@ -120,7 +104,7 @@ internal class FullQRCodeView @JvmOverloads constructor( if (amount != null) { val formattedAmount = CurrencyUtils.formatAmount( amount, - componentParams.shopperLocale + componentParams.shopperLocale, ) binding.textviewAmount.isVisible = true binding.textviewAmount.text = formattedAmount @@ -139,7 +123,7 @@ internal class FullQRCodeView @JvmOverloads constructor( binding.imageViewLogo.loadLogo( environment = delegate.componentParams.environment, txVariant = paymentMethodType, - size = LogoSize.LARGE + size = LogoSize.LARGE, ) } } @@ -157,25 +141,29 @@ internal class FullQRCodeView @JvmOverloads constructor( val minutesSecondsString = localizedContext.getString( R.string.checkout_qr_code_time_left_format, minutes, - seconds + seconds, ) binding.textViewTimer.text = localizedContext.getString( R.string.checkout_qr_code_pay_now_timer_text, - minutesSecondsString + minutesSecondsString, ) binding.progressIndicator.progress = timerData.progress } private fun handleEventFlow(event: QrCodeUIEvent) { when (event) { - QrCodeUIEvent.QrImageDownloadResult.Success -> { + Success -> { context.toast(localizedContext.getString(R.string.checkout_qr_code_download_image_succeeded)) } - is QrCodeUIEvent.QrImageDownloadResult.Failure -> { + PermissionDenied -> { + context.toast(localizedContext.getString(R.string.checkout_qr_code_permission_denied)) + } + + is Failure -> { context.toast(localizedContext.getString(R.string.checkout_qr_code_download_image_failed)) - Logger.e(TAG, "download file failed", event.throwable) + adyenLog(AdyenLogLevel.ERROR, event.throwable) { "download file failed" } } } } @@ -183,8 +171,4 @@ internal class FullQRCodeView @JvmOverloads constructor( override fun getView(): View = this override fun highlightValidationErrors() = Unit - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/SimpleQRCodeView.kt b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/SimpleQRCodeView.kt index e172958564..1d5f95e457 100644 --- a/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/SimpleQRCodeView.kt +++ b/qr-code/src/main/java/com/adyen/checkout/qrcode/internal/ui/view/SimpleQRCodeView.kt @@ -18,8 +18,8 @@ import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.TimerData import com.adyen.checkout.components.core.internal.util.copyTextToClipboard -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.qrcode.R import com.adyen.checkout.qrcode.databinding.SimpleQrcodeViewBinding import com.adyen.checkout.qrcode.internal.ui.QRCodeDelegate @@ -41,7 +41,7 @@ internal class SimpleQRCodeView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -85,7 +85,7 @@ internal class SimpleQRCodeView @JvmOverloads constructor( } private fun outputDataChanged(outputData: QRCodeOutputData) { - Logger.d(TAG, "outputDataChanged") + adyenLog(AdyenLogLevel.DEBUG) { "outputDataChanged" } updateMessageText(outputData.paymentMethodType) updateLogo(outputData.paymentMethodType) @@ -100,7 +100,7 @@ internal class SimpleQRCodeView @JvmOverloads constructor( } private fun updateLogo(paymentMethodType: String?) { - Logger.d(TAG, "updateLogo - $paymentMethodType") + adyenLog(AdyenLogLevel.DEBUG) { "updateLogo - $paymentMethodType" } if (!paymentMethodType.isNullOrEmpty()) { binding.imageViewLogo.loadLogo( environment = delegate.componentParams.environment, @@ -121,12 +121,12 @@ internal class SimpleQRCodeView @JvmOverloads constructor( val minutesSecondsString = localizedContext.getString( R.string.checkout_qr_code_time_left_format, minutes, - seconds + seconds, ) binding.textViewTimer.text = localizedContext.getString( R.string.checkout_qr_code_timer_text, - minutesSecondsString + minutesSecondsString, ) binding.progressIndicator.progress = timerData.progress } @@ -136,15 +136,11 @@ internal class SimpleQRCodeView @JvmOverloads constructor( context.copyTextToClipboard( "Pix Code", qrCodeData, - localizedContext.getString(R.string.checkout_qr_code_copied_toast) + localizedContext.getString(R.string.checkout_qr_code_copied_toast), ) } override fun getView(): View = this override fun highlightValidationErrors() = Unit - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/qr-code/src/main/res/template/values/strings.xml.tt b/qr-code/src/main/res/template/values/strings.xml.tt index 7f736d642e..58d1081e73 100644 --- a/qr-code/src/main/res/template/values/strings.xml.tt +++ b/qr-code/src/main/res/template/values/strings.xml.tt @@ -22,4 +22,5 @@ %%voucher.saveImage%% %%payNow.downloadImageSuccess%% %%payNow.downloadImageFailed%% + %%runtimePermission.permissionDeniedMessage%% diff --git a/qr-code/src/main/res/values-ar/strings.xml b/qr-code/src/main/res/values-ar/strings.xml index 3ab8f9a7fd..f4f616afb5 100644 --- a/qr-code/src/main/res/values-ar/strings.xml +++ b/qr-code/src/main/res/values-ar/strings.xml @@ -20,4 +20,5 @@ حفظ كصورة نجح تنزيل الصورة فشل تنزيل الصورة + لم يتم منح الإذن \ No newline at end of file diff --git a/qr-code/src/main/res/values-cs-rCZ/strings.xml b/qr-code/src/main/res/values-cs-rCZ/strings.xml index 47ff34edd6..92c4ceeed7 100644 --- a/qr-code/src/main/res/values-cs-rCZ/strings.xml +++ b/qr-code/src/main/res/values-cs-rCZ/strings.xml @@ -20,4 +20,5 @@ Uložit jako obrázek Obrázek se podařilo stáhnout Obrázek se nepodařilo stáhnout + Povolení se neuděluje \ No newline at end of file diff --git a/qr-code/src/main/res/values-da-rDK/strings.xml b/qr-code/src/main/res/values-da-rDK/strings.xml index 4396434b62..f7544af821 100644 --- a/qr-code/src/main/res/values-da-rDK/strings.xml +++ b/qr-code/src/main/res/values-da-rDK/strings.xml @@ -20,4 +20,5 @@ Gem som billede Billedet blev downloadet Billedet blev ikke downloadet + Tilladelse er ikke givet \ No newline at end of file diff --git a/qr-code/src/main/res/values-de-rDE/strings.xml b/qr-code/src/main/res/values-de-rDE/strings.xml index f1ea8e35bc..0205856b40 100644 --- a/qr-code/src/main/res/values-de-rDE/strings.xml +++ b/qr-code/src/main/res/values-de-rDE/strings.xml @@ -20,4 +20,5 @@ Als Bild speichern Herunterladen des Bildes erfolgreich Herunterladen des Bildes fehlgeschlagen + Berechtigung nicht erteilt \ No newline at end of file diff --git a/qr-code/src/main/res/values-el-rGR/strings.xml b/qr-code/src/main/res/values-el-rGR/strings.xml index a4f2c7dba9..7bfe9e3804 100644 --- a/qr-code/src/main/res/values-el-rGR/strings.xml +++ b/qr-code/src/main/res/values-el-rGR/strings.xml @@ -20,4 +20,5 @@ Αποθήκευση ως εικόνας Η εικόνα λήφθηκε επιτυχώς Η λήψη της εικόνας απέτυχε + Δεν εκχωρήθηκε δικαίωμα \ No newline at end of file diff --git a/qr-code/src/main/res/values-es-rES/strings.xml b/qr-code/src/main/res/values-es-rES/strings.xml index 9de40117f4..167fa8f7c1 100644 --- a/qr-code/src/main/res/values-es-rES/strings.xml +++ b/qr-code/src/main/res/values-es-rES/strings.xml @@ -20,4 +20,5 @@ Guardar como imagen La descarga de la imagen se realizó correctamente Error al descargar la imagen + No se ha dado permiso \ No newline at end of file diff --git a/qr-code/src/main/res/values-fi-rFI/strings.xml b/qr-code/src/main/res/values-fi-rFI/strings.xml index 9da47b8a5c..4b3ffec3ed 100644 --- a/qr-code/src/main/res/values-fi-rFI/strings.xml +++ b/qr-code/src/main/res/values-fi-rFI/strings.xml @@ -20,4 +20,5 @@ Tallenna kuvana Kuvan lataaminen onnistui Kuvan lataaminen epäonnistui + Lupaa ei myönnetty \ No newline at end of file diff --git a/qr-code/src/main/res/values-fr-rFR/strings.xml b/qr-code/src/main/res/values-fr-rFR/strings.xml index 91015f0503..d7c1d087b8 100644 --- a/qr-code/src/main/res/values-fr-rFR/strings.xml +++ b/qr-code/src/main/res/values-fr-rFR/strings.xml @@ -20,4 +20,5 @@ Enregistrer en tant qu\'image Téléchargement de l\'image réussi Échec du téléchargement de l\'image + L\'autorisation n\'a pas été accordée \ No newline at end of file diff --git a/qr-code/src/main/res/values-hr-rHR/strings.xml b/qr-code/src/main/res/values-hr-rHR/strings.xml index 71e8a7c984..95bffde599 100644 --- a/qr-code/src/main/res/values-hr-rHR/strings.xml +++ b/qr-code/src/main/res/values-hr-rHR/strings.xml @@ -20,4 +20,5 @@ Spremi kao sliku Preuzimanje slike je uspjelo Preuzimanje slike nije uspjelo + Dopuštenje nije dodijeljeno \ No newline at end of file diff --git a/qr-code/src/main/res/values-hu-rHU/strings.xml b/qr-code/src/main/res/values-hu-rHU/strings.xml index 1a4b062423..acc05ffe8b 100644 --- a/qr-code/src/main/res/values-hu-rHU/strings.xml +++ b/qr-code/src/main/res/values-hu-rHU/strings.xml @@ -20,4 +20,5 @@ Mentés képként A kép letöltése sikerült Nem sikerült letölteni a képet + Engedély nincs megadva \ No newline at end of file diff --git a/qr-code/src/main/res/values-it-rIT/strings.xml b/qr-code/src/main/res/values-it-rIT/strings.xml index ad58fdf8c4..48f3e1c530 100644 --- a/qr-code/src/main/res/values-it-rIT/strings.xml +++ b/qr-code/src/main/res/values-it-rIT/strings.xml @@ -20,4 +20,5 @@ Salva come immagine Download dell\'immagine riuscito Download dell\'immagine non riuscito + l\'autorizzazione non viene concessa \ No newline at end of file diff --git a/qr-code/src/main/res/values-ja-rJP/strings.xml b/qr-code/src/main/res/values-ja-rJP/strings.xml index 8ea69d1824..63bfd0a5c6 100644 --- a/qr-code/src/main/res/values-ja-rJP/strings.xml +++ b/qr-code/src/main/res/values-ja-rJP/strings.xml @@ -20,4 +20,5 @@ 画像として保存 画像のダウンロードに成功しました 画像のダウンロードに失敗しました + 権限が付与されていません \ No newline at end of file diff --git a/qr-code/src/main/res/values-ko-rKR/strings.xml b/qr-code/src/main/res/values-ko-rKR/strings.xml index 4d43e554fa..1e2fa8670e 100644 --- a/qr-code/src/main/res/values-ko-rKR/strings.xml +++ b/qr-code/src/main/res/values-ko-rKR/strings.xml @@ -20,4 +20,5 @@ 이미지로 저장 이미지 다운로드 성공 이미지 다운로드 실패 + 권한이 부여되지 않음 \ No newline at end of file diff --git a/qr-code/src/main/res/values-nb-rNO/strings.xml b/qr-code/src/main/res/values-nb-rNO/strings.xml index 95f46495a4..977d371ffc 100644 --- a/qr-code/src/main/res/values-nb-rNO/strings.xml +++ b/qr-code/src/main/res/values-nb-rNO/strings.xml @@ -20,4 +20,5 @@ Lagre som bilde Bildet er lastet ned Nedlastingen av bildet mislyktes + Tillatelse er ikke gitt \ No newline at end of file diff --git a/qr-code/src/main/res/values-nl-rNL/strings.xml b/qr-code/src/main/res/values-nl-rNL/strings.xml index 7b024cab19..3634c9f53f 100644 --- a/qr-code/src/main/res/values-nl-rNL/strings.xml +++ b/qr-code/src/main/res/values-nl-rNL/strings.xml @@ -20,4 +20,5 @@ Opslaan als afbeelding Afbeelding downloaden gelukt Afbeelding downloaden mislukt + Geen toestemming verleend \ No newline at end of file diff --git a/qr-code/src/main/res/values-pl-rPL/strings.xml b/qr-code/src/main/res/values-pl-rPL/strings.xml index 35991eb665..be14554d65 100644 --- a/qr-code/src/main/res/values-pl-rPL/strings.xml +++ b/qr-code/src/main/res/values-pl-rPL/strings.xml @@ -20,4 +20,5 @@ Zapisz jako obraz Pomyślnie pobrano obraz Pobieranie obrazu nie powiodło się + Zezwolenie nie zostało udzielone \ No newline at end of file diff --git a/qr-code/src/main/res/values-pt-rBR/strings.xml b/qr-code/src/main/res/values-pt-rBR/strings.xml index 308a79e3cb..9d6241fa06 100644 --- a/qr-code/src/main/res/values-pt-rBR/strings.xml +++ b/qr-code/src/main/res/values-pt-rBR/strings.xml @@ -20,4 +20,5 @@ Salvar como imagem A imagem foi baixada Não foi possível baixar imagem + Permissão não concedida \ No newline at end of file diff --git a/qr-code/src/main/res/values-pt-rPT/strings.xml b/qr-code/src/main/res/values-pt-rPT/strings.xml index b8b09f9dd9..11dc955f25 100644 --- a/qr-code/src/main/res/values-pt-rPT/strings.xml +++ b/qr-code/src/main/res/values-pt-rPT/strings.xml @@ -20,4 +20,5 @@ Guardar como imagem A transferência da imagem foi bem sucedida A transferência da imagem falhou + Não é concedida permissão \ No newline at end of file diff --git a/qr-code/src/main/res/values-ro-rRO/strings.xml b/qr-code/src/main/res/values-ro-rRO/strings.xml index 6f0a5f136d..35155b42b8 100644 --- a/qr-code/src/main/res/values-ro-rRO/strings.xml +++ b/qr-code/src/main/res/values-ro-rRO/strings.xml @@ -20,4 +20,5 @@ Salvați ca imagine Imaginea a fost descărcată Imaginea nu a putut fi descărcată + Nu se acordă permisiunea \ No newline at end of file diff --git a/qr-code/src/main/res/values-ru-rRU/strings.xml b/qr-code/src/main/res/values-ru-rRU/strings.xml index 477b502d17..5cf6286cd3 100644 --- a/qr-code/src/main/res/values-ru-rRU/strings.xml +++ b/qr-code/src/main/res/values-ru-rRU/strings.xml @@ -20,4 +20,5 @@ Сохранить как изображение Изображение загружено Сбой загрузки изображения + Разрешение не предоставлено \ No newline at end of file diff --git a/qr-code/src/main/res/values-sk-rSK/strings.xml b/qr-code/src/main/res/values-sk-rSK/strings.xml index 8ea8f04805..6b80b6c284 100644 --- a/qr-code/src/main/res/values-sk-rSK/strings.xml +++ b/qr-code/src/main/res/values-sk-rSK/strings.xml @@ -20,4 +20,5 @@ Uložiť ako obrázok Stiahnutie obrázka bolo úspešné Stiahnutie obrázka zlyhalo + Povolenie nebolo udelené \ No newline at end of file diff --git a/qr-code/src/main/res/values-sl-rSI/strings.xml b/qr-code/src/main/res/values-sl-rSI/strings.xml index fc738b0356..00a29947a2 100644 --- a/qr-code/src/main/res/values-sl-rSI/strings.xml +++ b/qr-code/src/main/res/values-sl-rSI/strings.xml @@ -20,4 +20,5 @@ Shrani kot sliko Prenos slike je uspel Prenos slike ni uspel + Dovoljenje ni bilo odobreno \ No newline at end of file diff --git a/qr-code/src/main/res/values-sv-rSE/strings.xml b/qr-code/src/main/res/values-sv-rSE/strings.xml index 1435a4fc54..a2ac5d02c2 100644 --- a/qr-code/src/main/res/values-sv-rSE/strings.xml +++ b/qr-code/src/main/res/values-sv-rSE/strings.xml @@ -20,4 +20,5 @@ Spara som bild Nedladdning av bild lyckades Nedladdning av bild misslyckades + Behörighet beviljas inte \ No newline at end of file diff --git a/qr-code/src/main/res/values-zh-rCN/strings.xml b/qr-code/src/main/res/values-zh-rCN/strings.xml index 8bfafc420e..9ad7c5ef2f 100644 --- a/qr-code/src/main/res/values-zh-rCN/strings.xml +++ b/qr-code/src/main/res/values-zh-rCN/strings.xml @@ -20,4 +20,5 @@ 另存为图片 下载图片成功 下载图片失败 + 未授予权限 \ No newline at end of file diff --git a/qr-code/src/main/res/values-zh-rTW/strings.xml b/qr-code/src/main/res/values-zh-rTW/strings.xml index ca3df4a69c..812a698f13 100644 --- a/qr-code/src/main/res/values-zh-rTW/strings.xml +++ b/qr-code/src/main/res/values-zh-rTW/strings.xml @@ -20,4 +20,5 @@ 儲存為圖像 已成功下載影像 無法下載影像 + 未授與權限 \ No newline at end of file diff --git a/qr-code/src/main/res/values/strings.xml b/qr-code/src/main/res/values/strings.xml index c0932ded5a..5620936460 100644 --- a/qr-code/src/main/res/values/strings.xml +++ b/qr-code/src/main/res/values/strings.xml @@ -22,4 +22,5 @@ Save as image Downloading image succeeded Downloading image failed + Permission is not granted \ No newline at end of file diff --git a/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeComponentTest.kt b/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeComponentTest.kt index 637d5298b8..4cb0276916 100644 --- a/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeComponentTest.kt +++ b/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeComponentTest.kt @@ -16,10 +16,9 @@ import app.cash.turbine.test import com.adyen.checkout.components.core.action.QrCodeAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.qrcode.internal.ui.QRCodeDelegate import com.adyen.checkout.qrcode.internal.ui.QrCodeComponentViewType +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -37,7 +36,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class QRCodeComponentTest( @Mock private val qrCodeDelegate: QRCodeDelegate, @Mock private val actionComponentEventHandler: ActionComponentEventHandler, @@ -47,8 +46,6 @@ internal class QRCodeComponentTest( @BeforeEach fun before() { - AdyenLogger.setLogLevel(Logger.NONE) - whenever(qrCodeDelegate.viewFlow) doReturn MutableStateFlow(QrCodeComponentViewType.SIMPLE_QR_CODE) component = QRCodeComponent(qrCodeDelegate, actionComponentEventHandler) } diff --git a/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeConfigurationTest.kt b/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeConfigurationTest.kt new file mode 100644 index 0000000000..8ff5b98be0 --- /dev/null +++ b/qr-code/src/test/java/com/adyen/checkout/qrcode/QRCodeConfigurationTest.kt @@ -0,0 +1,82 @@ +package com.adyen.checkout.qrcode + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class QRCodeConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + qrCode() + } + + val actual = checkoutConfiguration.getQRCodeConfiguration() + + val expected = QRCodeConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = QRCodeConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualQrCodeConfig = actual.getQRCodeConfiguration() + assertEquals(config.shopperLocale, actualQrCodeConfig?.shopperLocale) + assertEquals(config.environment, actualQrCodeConfig?.environment) + assertEquals(config.clientKey, actualQrCodeConfig?.clientKey) + assertEquals(config.amount, actualQrCodeConfig?.amount) + assertEquals(config.analyticsConfiguration, actualQrCodeConfig?.analyticsConfiguration) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/qr-code/src/test/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegateTest.kt b/qr-code/src/test/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegateTest.kt index dac8d27081..819a4c7ff9 100644 --- a/qr-code/src/test/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegateTest.kt +++ b/qr-code/src/test/java/com/adyen/checkout/qrcode/internal/ui/DefaultQRCodeDelegateTest.kt @@ -11,26 +11,36 @@ package com.adyen.checkout.qrcode.internal.ui import android.app.Activity import android.content.Context import android.content.Intent +import android.os.Parcel +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.QrCodeAction +import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.data.api.StatusRepository import com.adyen.checkout.components.core.internal.data.model.StatusResponse import com.adyen.checkout.components.core.internal.test.TestStatusRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.TimerData -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.Environment +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.FileDownloader -import com.adyen.checkout.core.internal.util.Logger -import com.adyen.checkout.qrcode.QRCodeConfiguration import com.adyen.checkout.qrcode.internal.QRCodeCountDownTimer import com.adyen.checkout.qrcode.internal.ui.model.QrCodeUIEvent +import com.adyen.checkout.qrcode.qrCode +import com.adyen.checkout.test.LoggingExtension +import com.adyen.checkout.ui.core.internal.RedirectHandler +import com.adyen.checkout.ui.core.internal.exception.PermissionRequestException import com.adyen.checkout.ui.core.internal.test.TestRedirectHandler +import com.adyen.checkout.ui.core.internal.util.ImageSaver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -44,50 +54,112 @@ import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.IOException import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class DefaultQRCodeDelegateTest( @Mock private val countDownTimer: QRCodeCountDownTimer, - @Mock private val context: Context + @Mock private val context: Context, + @Mock private val imageSaver: ImageSaver ) { private lateinit var redirectHandler: TestRedirectHandler private lateinit var statusRepository: TestStatusRepository private lateinit var paymentDataRepository: PaymentDataRepository private lateinit var delegate: DefaultQRCodeDelegate - private lateinit var fileDownloader: FileDownloader @BeforeEach fun beforeEach() { - fileDownloader = spy(FileDownloader(context)) statusRepository = TestStatusRepository() redirectHandler = TestRedirectHandler() paymentDataRepository = PaymentDataRepository(SavedStateHandle()) - val configuration = QRCodeConfiguration.Builder( - Locale.US, + val configuration = CheckoutConfiguration( Environment.TEST, - TEST_CLIENT_KEY - ).build() - delegate = DefaultQRCodeDelegate( + TEST_CLIENT_KEY, + ) { + qrCode() + } + delegate = createDelegate( observerRepository = ActionObserverRepository(), - componentParams = GenericComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()) + .mapToParams(configuration, Locale.US, null, null), statusRepository = statusRepository, statusCountDownTimer = countDownTimer, redirectHandler = redirectHandler, paymentDataRepository = paymentDataRepository, - fileDownloader = fileDownloader + imageSaver = imageSaver, + ) + } + + @Test + fun `when observe is called, then observers are being added to the repository`() { + val observerRepository = mock() + val delegate = createDelegate( + observerRepository = observerRepository, ) - AdyenLogger.setLogLevel(Logger.NONE) + val lifecycleOwner = mock().apply { + whenever(lifecycle).thenReturn(mock()) + } + val coroutineScope = mock() + val callback = mock<(ActionComponentEvent) -> Unit>() + + delegate.observe(lifecycleOwner, coroutineScope, callback) + + verify(observerRepository).addObservers( + detailsFlow = eq(delegate.detailsFlow), + exceptionFlow = eq(delegate.exceptionFlow), + permissionFlow = eq(delegate.permissionFlow), + lifecycleOwner = eq(lifecycleOwner), + coroutineScope = eq(coroutineScope), + callback = eq(callback), + ) + } + + @Test + fun `when removeObserver is called, then observers are being removed`() { + val observerRepository = mock() + val delegate = createDelegate( + observerRepository = observerRepository, + ) + + delegate.removeObserver() + + verify(observerRepository).removeObservers() + } + + @Test + fun `when handleAction is called with unsupported action, then an error should be emitted`() = runTest { + delegate.exceptionFlow.test { + delegate.handleAction( + createTestAction(), + mock(), + ) + + assert(expectMostRecentItem() is ComponentException) + } + } + + @Test + fun `when handleAction is called with null payment data, then an error should be emitted`() = runTest { + delegate.exceptionFlow.test { + delegate.handleAction( + QrCodeAction(paymentMethodType = PaymentMethodTypes.PIX, paymentData = null), + mock(), + ) + + assert(expectMostRecentItem() is ComponentException) + } } @Nested @@ -122,7 +194,7 @@ internal class DefaultQRCodeDelegateTest( QrCodeAction( paymentMethodType = PaymentMethodTypes.PIX, qrCodeData = "qrData", - paymentData = "paymentData" + paymentData = "paymentData", ), Activity(), ) @@ -202,7 +274,7 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `handleAction called, then simple qr view flow is updated`() = runTest { + fun `handleAction is called, then simple qr view flow is updated`() = runTest { delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.viewFlow.test { @@ -230,7 +302,7 @@ internal class DefaultQRCodeDelegateTest( QrCodeAction( paymentMethodType = PaymentMethodTypes.PAY_NOW, qrCodeData = "qrData", - paymentData = "paymentData" + paymentData = "paymentData", ), Activity(), ) @@ -306,7 +378,7 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `handleAction called, then full qr view flow is updated`() = runTest { + fun `handleAction is called, then full qr view flow is updated`() = runTest { delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.viewFlow.test { @@ -327,7 +399,7 @@ internal class DefaultQRCodeDelegateTest( inner class RedirectFlowTest { @Test - fun `handleAction called and RedirectHandler returns an error, then the error is propagated`() = runTest { + fun `handleAction is called and RedirectHandler returns an error, then the error is propagated`() = runTest { val error = ComponentException("Failed to make redirect.") redirectHandler.exception = error @@ -339,7 +411,7 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `handleAction called with valid data, then no error is propagated`() = runTest { + fun `handleAction is called with valid data, then no error is propagated`() = runTest { delegate.exceptionFlow.test { delegate.handleAction(QrCodeAction(paymentMethodType = "test", paymentData = "paymentData"), Activity()) @@ -348,7 +420,7 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `handleIntent called and RedirectHandler returns an error, then the error is propagated`() = runTest { + fun `handleIntent is called and RedirectHandler returns an error, then the error is propagated`() = runTest { val error = ComponentException("Failed to parse redirect result.") redirectHandler.exception = error @@ -360,7 +432,7 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `handleIntent called with valid data, then the details are emitted`() = runTest { + fun `handleIntent is called with valid data, then the details are emitted`() = runTest { delegate.detailsFlow.test { delegate.handleAction(QrCodeAction(paymentData = "paymentData"), Activity()) delegate.handleIntent(Intent()) @@ -373,7 +445,7 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `handleAction called, then the view flow is updated`() = runTest { + fun `handleAction is called, then the view flow is updated`() = runTest { delegate.viewFlow.test { assertNull(awaitItem()) @@ -385,38 +457,155 @@ internal class DefaultQRCodeDelegateTest( } @Test - fun `test download qr image with successful result`() = runTest { - delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + fun `when refreshStatus is called, then status for statusRepository gets refreshed`() = runTest { + val statusRepository = mock() + val paymentData = "Payment Data" + val delegate = createDelegate( + statusRepository = statusRepository, + paymentDataRepository = paymentDataRepository, + ).apply { + initialize(CoroutineScope(UnconfinedTestDispatcher())) + handleAction( + QrCodeAction(paymentMethodType = PaymentMethodTypes.PIX, paymentData = paymentData), + mock(), + ) + } + + delegate.refreshStatus() + + verify(statusRepository).refreshStatus(paymentData) + } + + @Test + fun `when refreshStatus is called with no payment data, then status for statusRepository does not get refreshed`() = + runTest { + val statusRepository = mock() + val delegate = createDelegate( + statusRepository = statusRepository, + paymentDataRepository = paymentDataRepository, + ).apply { + handleAction( + QrCodeAction(paymentMethodType = PaymentMethodTypes.PIX, paymentData = null), + mock(), + ) + } + delegate.refreshStatus() + + verify(statusRepository, never()).refreshStatus(any()) + } + + @Test + fun `when downloadQRImage is called with success, then Success gets emitted`() = runTest { + whenever(imageSaver.saveImageFromUrl(any(), any(), any(), anyOrNull(), anyOrNull())).thenReturn( + Result.success(Unit), + ) + + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.eventFlow.test { - val result = Result.success(Unit) - whenever(fileDownloader.download(anyString(), anyString(), anyString(), anyString())) doReturn result val expectedResult = QrCodeUIEvent.QrImageDownloadResult.Success - delegate.downloadQRImage() + delegate.downloadQRImage(context) assertEquals(expectedResult, expectMostRecentItem()) - verify(fileDownloader).download(anyString(), anyString(), anyString(), anyString()) } } @Test - fun `test download qr image with failure result`() = runTest { + fun `when downloadQRImage is called with permission exception, then PermissionDenied gets emitted`() = runTest { + whenever(imageSaver.saveImageFromUrl(any(), any(), any(), anyOrNull(), anyOrNull())).thenReturn( + Result.failure(PermissionRequestException("Error message for permission request exception")), + ) + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) + delegate.eventFlow.test { + val expectedResult = QrCodeUIEvent.QrImageDownloadResult.PermissionDenied + + delegate.downloadQRImage(context) + + assertEquals(expectedResult, expectMostRecentItem()) + } + } + + @Test + fun `when downloadQRImage is called with failure, then Success gets emitted`() = runTest { + val throwable = CheckoutException("error") + whenever(imageSaver.saveImageFromUrl(any(), any(), any(), anyOrNull(), anyOrNull())).thenReturn( + Result.failure(throwable), + ) + delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) delegate.eventFlow.test { - val throwable = CheckoutException("error") - val result = Result.failure(throwable) - whenever(fileDownloader.download(anyString(), anyString(), anyString(), anyString())) doReturn result val expectedResult = QrCodeUIEvent.QrImageDownloadResult.Failure(throwable) - delegate.downloadQRImage() + delegate.downloadQRImage(context) assertEquals(expectedResult, expectMostRecentItem()) - verify(fileDownloader).download(anyString(), anyString(), anyString(), anyString()) } } + @Test + fun `when requestPermission is called, then correct permission request data is being emitted`() = runTest { + val requiredPermission = "Required Permission" + val permissionCallback = mock() + + delegate.permissionFlow.test { + delegate.requestPermission(context, requiredPermission, permissionCallback) + + val mostRecentValue = expectMostRecentItem() + assertEquals(requiredPermission, mostRecentValue.requiredPermission) + assertEquals(permissionCallback, mostRecentValue.permissionCallback) + } + } + + @Test + fun `when onCleared is called, observers are removed`() { + val observerRepository = mock() + val countDownTimer = mock() + val redirectHandler = mock() + val delegate = createDelegate( + observerRepository = observerRepository, + statusCountDownTimer = countDownTimer, + redirectHandler = redirectHandler, + ) + + delegate.onCleared() + + verify(observerRepository).removeObservers() + verify(countDownTimer).cancel() + verify(redirectHandler).removeOnRedirectListener() + } + + private fun createTestAction( + type: String = "test", + paymentData: String = "paymentData", + paymentMethodType: String = "paymentMethodType", + ) = object : Action() { + override var type: String? = type + override var paymentData: String? = paymentData + override var paymentMethodType: String? = paymentMethodType + override fun writeToParcel(dest: Parcel, flags: Int) = Unit + } + + @Suppress("LongParameterList") + private fun createDelegate( + observerRepository: ActionObserverRepository = mock(), + componentParams: GenericComponentParams = mock(), + statusRepository: StatusRepository = mock(), + statusCountDownTimer: QRCodeCountDownTimer = mock(), + redirectHandler: RedirectHandler = mock(), + paymentDataRepository: PaymentDataRepository = mock(), + imageSaver: ImageSaver = mock(), + ) = DefaultQRCodeDelegate( + observerRepository = observerRepository, + componentParams = componentParams, + statusRepository = statusRepository, + statusCountDownTimer = statusCountDownTimer, + redirectHandler = redirectHandler, + paymentDataRepository = paymentDataRepository, + imageSaver = imageSaver, + ) + companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" } diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/RedirectComponent.kt b/redirect/src/main/java/com/adyen/checkout/redirect/RedirectComponent.kt index 850ead8670..13f7ab6b1d 100644 --- a/redirect/src/main/java/com/adyen/checkout/redirect/RedirectComponent.kt +++ b/redirect/src/main/java/com/adyen/checkout/redirect/RedirectComponent.kt @@ -20,8 +20,8 @@ import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler import com.adyen.checkout.components.core.internal.IntentHandlingComponent import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.redirect.internal.provider.RedirectComponentProvider import com.adyen.checkout.redirect.internal.ui.RedirectDelegate import com.adyen.checkout.ui.core.internal.ui.ComponentViewType @@ -78,12 +78,11 @@ class RedirectComponent internal constructor( override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } delegate.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER: ActionComponentProvider = diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/RedirectConfiguration.kt b/redirect/src/main/java/com/adyen/checkout/redirect/RedirectConfiguration.kt index 342e85017c..9cbba15cbc 100644 --- a/redirect/src/main/java/com/adyen/checkout/redirect/RedirectConfiguration.kt +++ b/redirect/src/main/java/com/adyen/checkout/redirect/RedirectConfiguration.kt @@ -10,8 +10,10 @@ package com.adyen.checkout.redirect import android.content.Context import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -21,7 +23,7 @@ import java.util.Locale */ @Parcelize class RedirectConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -33,6 +35,22 @@ class RedirectConfiguration private constructor( */ class Builder : BaseConfigurationBuilder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -40,14 +58,15 @@ class RedirectConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -56,7 +75,7 @@ class RedirectConfiguration private constructor( constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super( shopperLocale, environment, - clientKey + clientKey, ) override fun buildInternal(): RedirectConfiguration { @@ -70,3 +89,34 @@ class RedirectConfiguration private constructor( } } } + +fun CheckoutConfiguration.redirect( + configuration: @CheckoutConfigurationMarker RedirectConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = RedirectConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addActionConfiguration(config) + return this +} + +fun CheckoutConfiguration.getRedirectConfiguration(): RedirectConfiguration? { + return getActionConfiguration(RedirectConfiguration::class.java) +} + +internal fun RedirectConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addActionConfiguration(this@toCheckoutConfiguration) + } +} diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/api/NativeRedirectService.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/api/NativeRedirectService.kt new file mode 100644 index 0000000000..afadbb867d --- /dev/null +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/api/NativeRedirectService.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 17/10/2023. + */ + +package com.adyen.checkout.redirect.internal.data.api + +import com.adyen.checkout.core.internal.data.api.HttpClient +import com.adyen.checkout.core.internal.data.api.post +import com.adyen.checkout.redirect.internal.data.model.NativeRedirectRequest +import com.adyen.checkout.redirect.internal.data.model.NativeRedirectResponse +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +internal class NativeRedirectService( + private val httpClient: HttpClient, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + suspend fun makeNativeRedirect( + request: NativeRedirectRequest, + clientKey: String, + ): NativeRedirectResponse = withContext(dispatcher) { + httpClient.post( + path = "v1/nativeRedirect/redirectResult", + queryParameters = mapOf("clientKey" to clientKey), + body = request, + requestSerializer = NativeRedirectRequest.SERIALIZER, + responseSerializer = NativeRedirectResponse.SERIALIZER, + ) + } +} diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectRequest.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectRequest.kt new file mode 100644 index 0000000000..efbcc16d52 --- /dev/null +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectRequest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 17/10/2023. + */ + +package com.adyen.checkout.redirect.internal.data.model + +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.data.model.ModelObject +import kotlinx.parcelize.Parcelize +import org.json.JSONException +import org.json.JSONObject + +@Parcelize +internal data class NativeRedirectRequest( + val redirectData: String, + val returnQueryString: String, +) : ModelObject() { + + companion object { + private const val REDIRECT_DATA = "redirectData" + private const val RETURN_QUERY_STRING = "returnQueryString" + + @JvmField + val SERIALIZER: Serializer = + object : Serializer { + override fun serialize(modelObject: NativeRedirectRequest): JSONObject { + val jsonObject = JSONObject() + try { + jsonObject.putOpt(REDIRECT_DATA, modelObject.redirectData) + jsonObject.putOpt(RETURN_QUERY_STRING, modelObject.returnQueryString) + } catch (e: JSONException) { + throw ModelSerializationException(NativeRedirectRequest::class.java, e) + } + return jsonObject + } + + override fun deserialize(jsonObject: JSONObject): NativeRedirectRequest { + return try { + NativeRedirectRequest( + redirectData = jsonObject.getString(REDIRECT_DATA), + returnQueryString = jsonObject.getString(RETURN_QUERY_STRING), + ) + } catch (e: JSONException) { + throw ModelSerializationException(NativeRedirectRequest::class.java, e) + } + } + } + } +} diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectResponse.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectResponse.kt new file mode 100644 index 0000000000..c0b43b6f72 --- /dev/null +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectResponse.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 17/10/2023. + */ + +package com.adyen.checkout.redirect.internal.data.model + +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.data.model.ModelObject +import kotlinx.parcelize.Parcelize +import org.json.JSONException +import org.json.JSONObject + +@Parcelize +internal data class NativeRedirectResponse( + val redirectResult: String, +) : ModelObject() { + + companion object { + private const val REDIRECT_RESULT = "redirectResult" + + @JvmField + val SERIALIZER: Serializer = + object : Serializer { + override fun serialize(modelObject: NativeRedirectResponse): JSONObject { + val jsonObject = JSONObject() + try { + jsonObject.putOpt(REDIRECT_RESULT, modelObject.redirectResult) + } catch (e: JSONException) { + throw ModelSerializationException(NativeRedirectResponse::class.java, e) + } + return jsonObject + } + + override fun deserialize(jsonObject: JSONObject): NativeRedirectResponse { + return try { + NativeRedirectResponse( + redirectResult = jsonObject.getString(REDIRECT_RESULT), + ) + } catch (e: JSONException) { + throw ModelSerializationException(NativeRedirectRequest::class.java, e) + } + } + } + } +} diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt index 6a01d7ff4d..07a742fc9d 100644 --- a/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/provider/RedirectComponentProvider.kt @@ -16,46 +16,50 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.components.core.ActionComponentCallback +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.components.core.action.ActionTypes import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.DefaultActionComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentDataRepository import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.SessionParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory +import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.redirect.RedirectComponent import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.redirect.internal.data.api.NativeRedirectService import com.adyen.checkout.redirect.internal.ui.DefaultRedirectDelegate import com.adyen.checkout.redirect.internal.ui.RedirectDelegate +import com.adyen.checkout.redirect.toCheckoutConfiguration import com.adyen.checkout.ui.core.internal.DefaultRedirectHandler class RedirectComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : ActionComponentProvider { - private val componentParamsMapper = GenericComponentParamsMapper(overrideComponentParams, overrideSessionParams) - override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, application: Application, - configuration: RedirectConfiguration, + checkoutConfiguration: CheckoutConfiguration, callback: ActionComponentCallback, - key: String?, + key: String? ): RedirectComponent { val redirectFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val redirectDelegate = getDelegate(configuration, savedStateHandle, application) + val redirectDelegate = getDelegate(checkoutConfiguration, savedStateHandle, application) RedirectComponent( delegate = redirectDelegate, - actionComponentEventHandler = DefaultActionComponentEventHandler(callback) + actionComponentEventHandler = DefaultActionComponentEventHandler(callback), ) } return ViewModelProvider(viewModelStoreOwner, redirectFactory)[key, RedirectComponent::class.java] @@ -65,23 +69,53 @@ constructor( } override fun getDelegate( - configuration: RedirectConfiguration, + checkoutConfiguration: CheckoutConfiguration, savedStateHandle: SavedStateHandle, - application: Application, + application: Application ): RedirectDelegate { - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + ) + val redirectHandler = DefaultRedirectHandler() val paymentDataRepository = PaymentDataRepository(savedStateHandle) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) + val nativeRedirectService = NativeRedirectService(httpClient) + return DefaultRedirectDelegate( observerRepository = ActionObserverRepository(), componentParams = componentParams, redirectHandler = redirectHandler, - paymentDataRepository = paymentDataRepository + paymentDataRepository = paymentDataRepository, + nativeRedirectService = nativeRedirectService, + ) + } + + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + application: Application, + configuration: RedirectConfiguration, + callback: ActionComponentCallback, + key: String?, + ): RedirectComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + application = application, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + callback = callback, + key = key, ) } override val supportedActionTypes: List - get() = listOf(RedirectAction.ACTION_TYPE) + get() = listOf(RedirectAction.ACTION_TYPE, ActionTypes.NATIVE_REDIRECT) override fun canHandleAction(action: Action): Boolean { return supportedActionTypes.contains(action.type) diff --git a/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt b/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt index e0d3441933..3ed7fa0af0 100644 --- a/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt +++ b/redirect/src/main/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegate.kt @@ -13,16 +13,22 @@ import android.content.Intent import androidx.lifecycle.LifecycleOwner import com.adyen.checkout.components.core.ActionComponentData import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.components.core.action.ActionTypes import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams import com.adyen.checkout.components.core.internal.util.bufferedChannel +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.exception.HttpException +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.redirect.internal.data.api.NativeRedirectService +import com.adyen.checkout.redirect.internal.data.model.NativeRedirectRequest +import com.adyen.checkout.redirect.internal.data.model.NativeRedirectResponse import com.adyen.checkout.ui.core.internal.RedirectHandler import com.adyen.checkout.ui.core.internal.ui.ComponentViewType import kotlinx.coroutines.CoroutineScope @@ -30,13 +36,16 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch import org.json.JSONObject +@Suppress("TooManyFunctions") internal class DefaultRedirectDelegate( private val observerRepository: ActionObserverRepository, override val componentParams: GenericComponentParams, private val redirectHandler: RedirectHandler, private val paymentDataRepository: PaymentDataRepository, + private val nativeRedirectService: NativeRedirectService, ) : RedirectDelegate { private val detailsChannel: Channel = bufferedChannel() @@ -47,8 +56,11 @@ internal class DefaultRedirectDelegate( override val viewFlow: Flow = MutableStateFlow(RedirectComponentViewType) + private var _coroutineScope: CoroutineScope? = null + private val coroutineScope: CoroutineScope get() = requireNotNull(_coroutineScope) + override fun initialize(coroutineScope: CoroutineScope) { - // no ops + _coroutineScope = coroutineScope } override fun observe( @@ -59,9 +71,10 @@ internal class DefaultRedirectDelegate( observerRepository.addObservers( detailsFlow = detailsFlow, exceptionFlow = exceptionFlow, + permissionFlow = null, lifecycleOwner = lifecycleOwner, coroutineScope = coroutineScope, - callback = callback + callback = callback, ) } @@ -75,13 +88,22 @@ internal class DefaultRedirectDelegate( return } - paymentDataRepository.paymentData = action.paymentData + when (action.type) { + ActionTypes.NATIVE_REDIRECT -> { + paymentDataRepository.nativeRedirectData = action.nativeRedirectData + } + + else -> { + paymentDataRepository.paymentData = action.paymentData + } + } + makeRedirect(activity, action.url) } private fun makeRedirect(activity: Activity, url: String?) { try { - Logger.d(TAG, "makeRedirect - $url") + adyenLog(AdyenLogLevel.DEBUG) { "makeRedirect - $url" } // TODO look into emitting a value to tell observers that a redirect was launched so they can track its // status when the app resumes. Currently we have no way of doing that but we can create something like // PaymentComponentState for actions. @@ -94,7 +116,16 @@ internal class DefaultRedirectDelegate( override fun handleIntent(intent: Intent) { try { val details = redirectHandler.parseRedirectResult(intent.data) - detailsChannel.trySend(createActionComponentData(details)) + val nativeRedirectData = paymentDataRepository.nativeRedirectData + when { + nativeRedirectData != null -> { + handleNativeRedirect(nativeRedirectData, details) + } + + else -> { + detailsChannel.trySend(createActionComponentData(details)) + } + } } catch (ex: CheckoutException) { exceptionChannel.trySend(ex) } @@ -107,6 +138,24 @@ internal class DefaultRedirectDelegate( ) } + private fun handleNativeRedirect(nativeRedirectData: String, details: JSONObject) { + coroutineScope.launch { + val request = NativeRedirectRequest( + redirectData = nativeRedirectData, + returnQueryString = details.optString(RETURN_URL_QUERY_STRING_PARAMETER), + ) + try { + val response = nativeRedirectService.makeNativeRedirect(request, componentParams.clientKey) + val detailsJson = NativeRedirectResponse.SERIALIZER.serialize(response) + detailsChannel.trySend(createActionComponentData(detailsJson)) + } catch (e: HttpException) { + onError(e) + } catch (e: ModelSerializationException) { + onError(e) + } + } + } + override fun onError(e: CheckoutException) { exceptionChannel.trySend(e) } @@ -118,9 +167,10 @@ internal class DefaultRedirectDelegate( override fun onCleared() { removeObserver() redirectHandler.removeOnRedirectListener() + _coroutineScope = null } companion object { - private val TAG = LogUtil.getTag() + private const val RETURN_URL_QUERY_STRING_PARAMETER = "returnUrlQueryString" } } diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/RedirectComponentTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/RedirectComponentTest.kt index 70838dcf8b..e9e20ff6d8 100644 --- a/redirect/src/test/java/com/adyen/checkout/redirect/RedirectComponentTest.kt +++ b/redirect/src/test/java/com/adyen/checkout/redirect/RedirectComponentTest.kt @@ -16,10 +16,9 @@ import app.cash.turbine.test import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.internal.ActionComponentEvent import com.adyen.checkout.components.core.internal.ActionComponentEventHandler -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.redirect.internal.ui.RedirectComponentViewType import com.adyen.checkout.redirect.internal.ui.RedirectDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -38,7 +37,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class RedirectComponentTest( @Mock private val redirectDelegate: RedirectDelegate, @Mock private val actionComponentEventHandler: ActionComponentEventHandler, @@ -48,8 +47,6 @@ internal class RedirectComponentTest( @BeforeEach fun before() { - AdyenLogger.setLogLevel(Logger.NONE) - whenever(redirectDelegate.viewFlow) doReturn MutableStateFlow(RedirectComponentViewType) component = RedirectComponent(redirectDelegate, actionComponentEventHandler) } diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/RedirectConfigurationTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/RedirectConfigurationTest.kt new file mode 100644 index 0000000000..c0fa4c31c2 --- /dev/null +++ b/redirect/src/test/java/com/adyen/checkout/redirect/RedirectConfigurationTest.kt @@ -0,0 +1,82 @@ +package com.adyen.checkout.redirect + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class RedirectConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + redirect() + } + + val actual = checkoutConfiguration.getRedirectConfiguration() + + val expected = RedirectConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = RedirectConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualRedirectConfig = actual.getRedirectConfiguration() + assertEquals(config.shopperLocale, actualRedirectConfig?.shopperLocale) + assertEquals(config.environment, actualRedirectConfig?.environment) + assertEquals(config.clientKey, actualRedirectConfig?.clientKey) + assertEquals(config.amount, actualRedirectConfig?.amount) + assertEquals(config.analyticsConfiguration, actualRedirectConfig?.analyticsConfiguration) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/api/NativeRedirectServiceTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/api/NativeRedirectServiceTest.kt new file mode 100644 index 0000000000..f438834d33 --- /dev/null +++ b/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/api/NativeRedirectServiceTest.kt @@ -0,0 +1,49 @@ +package com.adyen.checkout.redirect.internal.data.api + +import com.adyen.checkout.core.internal.data.api.HttpClient +import com.adyen.checkout.redirect.internal.data.model.NativeRedirectRequest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(MockitoExtension::class) +internal class NativeRedirectServiceTest( + @Mock private val httpClient: HttpClient +) { + + private lateinit var nativeRedirectService: NativeRedirectService + + @BeforeEach + fun setup() { + nativeRedirectService = NativeRedirectService( + httpClient = httpClient, + ) + } + + @Test + fun `makeNativeRedirect follows contract`() = runTest { + val request = NativeRedirectRequest( + redirectData = "redirectData", + returnQueryString = "returnQueryString", + ) + val clientKey = "clientKey" + + // Ignore any error since we only want to check if the call to post is made correctly. + runCatching { + nativeRedirectService.makeNativeRedirect(request, clientKey) + } + + verify(httpClient).post( + path = "v1/nativeRedirect/redirectResult", + queryParameters = mapOf("clientKey" to clientKey), + jsonBody = NativeRedirectRequest.SERIALIZER.serialize(request).toString(), + headers = emptyMap(), + ) + } +} diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectRequestTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectRequestTest.kt new file mode 100644 index 0000000000..b7f255ee60 --- /dev/null +++ b/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectRequestTest.kt @@ -0,0 +1,52 @@ +package com.adyen.checkout.redirect.internal.data.model + +import com.adyen.checkout.core.exception.ModelSerializationException +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +internal class NativeRedirectRequestTest { + + @Test + fun `when serializing, then all fields should be serialized correctly`() { + val request = NativeRedirectRequest( + redirectData = "testData", + returnQueryString = "testReturnString", + ) + + val actual = NativeRedirectRequest.SERIALIZER.serialize(request) + + val expected = JSONObject() + .put("redirectData", "testData") + .put("returnQueryString", "testReturnString") + + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun `when deserializing, then all fields should be deserializing correctly`() { + val response = JSONObject() + .put("redirectData", "testData") + .put("returnQueryString", "testReturnString") + + val actual = NativeRedirectRequest.SERIALIZER.deserialize(response) + + val expected = NativeRedirectRequest( + redirectData = "testData", + returnQueryString = "testReturnString", + ) + + assertEquals(expected, actual) + } + + @Test + fun `when deserializing and a field is missing, then an error is thrown`() { + val response = JSONObject() + .put("redirectData", "testData") + + assertThrows { + NativeRedirectRequest.SERIALIZER.deserialize(response) + } + } +} diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectResponseTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectResponseTest.kt new file mode 100644 index 0000000000..1109d5c5ba --- /dev/null +++ b/redirect/src/test/java/com/adyen/checkout/redirect/internal/data/model/NativeRedirectResponseTest.kt @@ -0,0 +1,47 @@ +package com.adyen.checkout.redirect.internal.data.model + +import com.adyen.checkout.core.exception.ModelSerializationException +import org.json.JSONObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +internal class NativeRedirectResponseTest { + + @Test + fun `when serializing, then all fields should be serialized correctly`() { + val request = NativeRedirectResponse( + redirectResult = "testRedirectResult", + ) + + val actual = NativeRedirectResponse.SERIALIZER.serialize(request) + + val expected = JSONObject() + .put("redirectResult", "testRedirectResult") + + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun `when deserializing, then all fields should be deserializing correctly`() { + val response = JSONObject() + .put("redirectResult", "testRedirectResult") + + val actual = NativeRedirectResponse.SERIALIZER.deserialize(response) + + val expected = NativeRedirectResponse( + redirectResult = "testRedirectResult", + ) + + assertEquals(expected, actual) + } + + @Test + fun `when deserializing and a field is missing, then an error is thrown`() { + val response = JSONObject() + + assertThrows { + NativeRedirectResponse.SERIALIZER.deserialize(response) + } + } +} diff --git a/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt b/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt index a30c3ee9f8..212d6f1568 100644 --- a/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt +++ b/redirect/src/test/java/com/adyen/checkout/redirect/internal/ui/DefaultRedirectDelegateTest.kt @@ -12,23 +12,46 @@ import android.app.Activity import android.content.Intent import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.action.ActionTypes import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.internal.ActionObserverRepository import com.adyen.checkout.components.core.internal.PaymentDataRepository +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParamsMapper import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.redirect.RedirectConfiguration +import com.adyen.checkout.core.exception.HttpException +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.redirect.internal.data.api.NativeRedirectService +import com.adyen.checkout.redirect.internal.data.model.NativeRedirectResponse +import com.adyen.checkout.redirect.redirect import com.adyen.checkout.ui.core.internal.test.TestRedirectHandler import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import java.util.Locale @OptIn(ExperimentalCoroutinesApi::class) -internal class DefaultRedirectDelegateTest { +@ExtendWith(MockitoExtension::class) +internal class DefaultRedirectDelegateTest( + @Mock private val nativeRedirectService: NativeRedirectService, +) { private lateinit var redirectHandler: TestRedirectHandler private lateinit var paymentDataRepository: PaymentDataRepository @@ -36,19 +59,11 @@ internal class DefaultRedirectDelegateTest { @BeforeEach fun beforeEach() { - val configuration = RedirectConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ).build() redirectHandler = TestRedirectHandler() paymentDataRepository = PaymentDataRepository(SavedStateHandle()) - delegate = DefaultRedirectDelegate( - ActionObserverRepository(), - GenericComponentParamsMapper(null, null).mapToParams(configuration, null), - redirectHandler, - paymentDataRepository - ) + redirectHandler = TestRedirectHandler() + paymentDataRepository = PaymentDataRepository(SavedStateHandle()) + delegate = createDelegate() } @Test @@ -72,6 +87,17 @@ internal class DefaultRedirectDelegateTest { } } + @Test + fun `when handleAction called with native redirect type, then the native redirect data should be stored`() { + val testData = "sometestdata" + delegate.handleAction( + action = RedirectAction(type = ActionTypes.NATIVE_REDIRECT, nativeRedirectData = testData), + activity = Activity(), + ) + + assertEquals(testData, paymentDataRepository.nativeRedirectData) + } + @Test fun `when handleIntent called and RedirectHandler returns an error, then the error is propagated`() = runTest { val error = ComponentException("Failed to parse redirect result.") @@ -97,7 +123,86 @@ internal class DefaultRedirectDelegateTest { } } + @Test + fun `when handleIntent called with valid data and it's a native redirect, then the details are emitted`() = + runTest { + delegate.detailsFlow.test { + val response = NativeRedirectResponse("someRedirectResult") + whenever(nativeRedirectService.makeNativeRedirect(any(), any())) doReturn response + delegate.initialize(this@runTest) + delegate.handleAction( + action = RedirectAction(type = ActionTypes.NATIVE_REDIRECT, nativeRedirectData = "testData"), + activity = Activity(), + ) + + delegate.handleIntent(Intent()) + + with(awaitItem()) { + assertEquals(NativeRedirectResponse.SERIALIZER.serialize(response).toString(), details.toString()) + assertNull(paymentData) + } + } + } + + @ParameterizedTest + @MethodSource("errorSource") + fun `when native redirect is handled and throws an error, then the error is propagated`(error: Exception) = + runTest { + delegate.exceptionFlow.test { + whenever(nativeRedirectService.makeNativeRedirect(any(), any())) doAnswer { throw error } + delegate.initialize(this@runTest) + delegate.handleAction( + action = RedirectAction(type = ActionTypes.NATIVE_REDIRECT, nativeRedirectData = "testData"), + activity = Activity(), + ) + + delegate.handleIntent(Intent()) + + assertEquals(error, awaitItem()) + } + } + + @Test + fun `when onCleared is called, then the delegate gets clean up correctly`() { + val mockObserverRepository = mock() + delegate = createDelegate(observerRepository = mockObserverRepository) + + delegate.onCleared() + + verify(mockObserverRepository).removeObservers() + redirectHandler.assertRemoveOnRedirectListenerCalled() + } + + private fun createDelegate( + observerRepository: ActionObserverRepository = ActionObserverRepository() + ): DefaultRedirectDelegate { + val configuration = CheckoutConfiguration( + Environment.TEST, + TEST_CLIENT_KEY, + ) { + redirect() + } + return DefaultRedirectDelegate( + observerRepository = observerRepository, + componentParams = GenericComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + configuration, + Locale.US, + null, + null, + ), + redirectHandler = redirectHandler, + paymentDataRepository = paymentDataRepository, + nativeRedirectService = nativeRedirectService, + ) + } + companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + + @JvmStatic + fun errorSource() = listOf( + arguments(HttpException(401, "", null)), + arguments(ModelSerializationException(NativeRedirectResponse::class.java, null)), + ) } } diff --git a/sepa/src/main/java/com/adyen/checkout/sepa/SepaComponent.kt b/sepa/src/main/java/com/adyen/checkout/sepa/SepaComponent.kt index 4106f178b2..df07c98d30 100644 --- a/sepa/src/main/java/com/adyen/checkout/sepa/SepaComponent.kt +++ b/sepa/src/main/java/com/adyen/checkout/sepa/SepaComponent.kt @@ -20,8 +20,8 @@ import com.adyen.checkout.components.core.internal.PaymentComponent import com.adyen.checkout.components.core.internal.PaymentComponentEvent import com.adyen.checkout.components.core.internal.toActionCallback import com.adyen.checkout.components.core.internal.ui.ComponentDelegate -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.sepa.internal.provider.SepaComponentProvider import com.adyen.checkout.sepa.internal.ui.SepaDelegate import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate @@ -74,24 +74,24 @@ class SepaComponent internal constructor( override fun isConfirmationRequired(): Boolean = sepaDelegate.isConfirmationRequired() override fun submit() { - (delegate as? ButtonDelegate)?.onSubmit() ?: Logger.e(TAG, "Component is currently not submittable, ignoring.") + (delegate as? ButtonDelegate)?.onSubmit() + ?: adyenLog(AdyenLogLevel.ERROR) { "Component is currently not submittable, ignoring." } } override fun setInteractionBlocked(isInteractionBlocked: Boolean) { (delegate as? SepaDelegate)?.setInteractionBlocked(isInteractionBlocked) - ?: Logger.e(TAG, "Payment component is not interactable, ignoring.") + ?: adyenLog(AdyenLogLevel.ERROR) { "Payment component is not interactable, ignoring." } } override fun onCleared() { super.onCleared() - Logger.d(TAG, "onCleared") + adyenLog(AdyenLogLevel.DEBUG) { "onCleared" } sepaDelegate.onCleared() genericActionDelegate.onCleared() componentEventHandler.onCleared() } companion object { - private val TAG = LogUtil.getTag() @JvmField val PROVIDER = SepaComponentProvider() diff --git a/sepa/src/main/java/com/adyen/checkout/sepa/SepaConfiguration.kt b/sepa/src/main/java/com/adyen/checkout/sepa/SepaConfiguration.kt index 7970b63ab7..126568c3b9 100644 --- a/sepa/src/main/java/com/adyen/checkout/sepa/SepaConfiguration.kt +++ b/sepa/src/main/java/com/adyen/checkout/sepa/SepaConfiguration.kt @@ -12,9 +12,12 @@ import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes import com.adyen.checkout.components.core.internal.ButtonConfiguration import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import kotlinx.parcelize.Parcelize import java.util.Locale @@ -25,7 +28,7 @@ import java.util.Locale @Parcelize @Suppress("LongParameterList") class SepaConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -43,6 +46,22 @@ class SepaConfiguration private constructor( private var isSubmitButtonVisible: Boolean? = null + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -50,14 +69,15 @@ class SepaConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -94,3 +114,38 @@ class SepaConfiguration private constructor( } } } + +fun CheckoutConfiguration.sepa( + configuration: @CheckoutConfigurationMarker SepaConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = SepaConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.SEPA, config) + return this +} + +fun CheckoutConfiguration.getSepaConfiguration(): SepaConfiguration? { + return getConfiguration(PaymentMethodTypes.SEPA) +} + +internal fun SepaConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.SEPA, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/sepa/src/main/java/com/adyen/checkout/sepa/internal/provider/SepaComponentProvider.kt b/sepa/src/main/java/com/adyen/checkout/sepa/internal/provider/SepaComponentProvider.kt index 07a984ff4e..e2a4eba75f 100644 --- a/sepa/src/main/java/com/adyen/checkout/sepa/internal/provider/SepaComponentProvider.kt +++ b/sepa/src/main/java/com/adyen/checkout/sepa/internal/provider/SepaComponentProvider.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.ComponentCallback import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod @@ -28,16 +29,19 @@ import com.adyen.checkout.components.core.internal.data.api.AnalyticsService import com.adyen.checkout.components.core.internal.data.api.DefaultAnalyticsRepository import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.internal.util.get import com.adyen.checkout.components.core.internal.util.viewModelFactory import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.data.api.HttpClientFactory +import com.adyen.checkout.core.internal.util.LocaleProvider import com.adyen.checkout.sepa.SepaComponent import com.adyen.checkout.sepa.SepaComponentState import com.adyen.checkout.sepa.SepaConfiguration +import com.adyen.checkout.sepa.getSepaConfiguration import com.adyen.checkout.sepa.internal.ui.DefaultSepaDelegate +import com.adyen.checkout.sepa.toCheckoutConfiguration import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -52,32 +56,30 @@ import com.adyen.checkout.ui.core.internal.ui.SubmitHandler class SepaComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + private val dropInOverrideParams: DropInOverrideParams? = null, private val analyticsRepository: AnalyticsRepository? = null, + private val localeProvider: LocaleProvider = LocaleProvider(), ) : PaymentComponentProvider< SepaComponent, SepaConfiguration, SepaComponentState, - ComponentCallback + ComponentCallback, >, SessionPaymentComponentProvider< SepaComponent, SepaConfiguration, SepaComponentState, - SessionComponentCallback + SessionComponentCallback, > { - private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams) - @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, viewModelStoreOwner: ViewModelStoreOwner, lifecycleOwner: LifecycleOwner, paymentMethod: PaymentMethod, - configuration: SepaConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: ComponentCallback, order: Order?, @@ -86,7 +88,13 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams(configuration, null) + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = null, + componentConfiguration = checkoutConfiguration.getSepaConfiguration(), + ) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( @@ -95,7 +103,7 @@ constructor( paymentMethod = paymentMethod, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -109,8 +117,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -119,7 +127,7 @@ constructor( sepaDelegate = sepaDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, sepaDelegate), - componentEventHandler = DefaultComponentEventHandler() + componentEventHandler = DefaultComponentEventHandler(), ) } @@ -131,6 +139,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + paymentMethod: PaymentMethod, + configuration: SepaConfiguration, + application: Application, + componentCallback: ComponentCallback, + order: Order?, + key: String? + ): SepaComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + order = order, + key = key, + ) + } + @Suppress("LongMethod") override fun get( savedStateRegistryOwner: SavedStateRegistryOwner, @@ -138,7 +170,7 @@ constructor( lifecycleOwner: LifecycleOwner, checkoutSession: CheckoutSession, paymentMethod: PaymentMethod, - configuration: SepaConfiguration, + checkoutConfiguration: CheckoutConfiguration, application: Application, componentCallback: SessionComponentCallback, key: String? @@ -146,10 +178,14 @@ constructor( assertSupported(paymentMethod) val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> - val componentParams = componentParamsMapper.mapToParams( - configuration = configuration, - sessionParams = SessionParamsFactory.create(checkoutSession), + val componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = checkoutConfiguration, + deviceLocale = localeProvider.getLocale(application), + dropInOverrideParams = dropInOverrideParams, + componentSessionParams = SessionParamsFactory.create(checkoutSession), + componentConfiguration = checkoutConfiguration.getSepaConfiguration(), ) + val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( @@ -160,7 +196,7 @@ constructor( sessionId = checkoutSession.sessionSetupResponse.id, ), analyticsService = AnalyticsService( - HttpClientFactory.getAnalyticsHttpClient(componentParams.environment) + HttpClientFactory.getAnalyticsHttpClient(componentParams.environment), ), analyticsMapper = AnalyticsMapper(), ) @@ -174,8 +210,8 @@ constructor( submitHandler = SubmitHandler(savedStateHandle), ) - val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate( - configuration = configuration.genericActionConfiguration, + val genericActionDelegate = GenericActionComponentProvider(dropInOverrideParams).getDelegate( + checkoutConfiguration = checkoutConfiguration, savedStateHandle = savedStateHandle, application = application, ) @@ -190,7 +226,7 @@ constructor( clientKey = componentParams.clientKey, ), sessionModel = sessionSavedStateHandleContainer.getSessionModel(), - isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false + isFlowTakenOver = sessionSavedStateHandleContainer.isFlowTakenOver ?: false, ) val sessionComponentEventHandler = SessionComponentEventHandler( sessionInteractor = sessionInteractor, @@ -201,7 +237,7 @@ constructor( sepaDelegate = sepaDelegate, genericActionDelegate = genericActionDelegate, actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, sepaDelegate), - componentEventHandler = sessionComponentEventHandler + componentEventHandler = sessionComponentEventHandler, ) } @@ -213,6 +249,30 @@ constructor( } } + override fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + configuration: SepaConfiguration, + application: Application, + componentCallback: SessionComponentCallback, + key: String? + ): SepaComponent { + return get( + savedStateRegistryOwner = savedStateRegistryOwner, + viewModelStoreOwner = viewModelStoreOwner, + lifecycleOwner = lifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = configuration.toCheckoutConfiguration(), + application = application, + componentCallback = componentCallback, + key = key, + ) + } + private fun assertSupported(paymentMethod: PaymentMethod) { if (!isPaymentMethodSupported(paymentMethod)) { throw ComponentException("Unsupported payment method ${paymentMethod.type}") diff --git a/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegate.kt b/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegate.kt index 9efcf29c80..ad521141d1 100644 --- a/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegate.kt +++ b/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegate.kt @@ -19,8 +19,8 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParams import com.adyen.checkout.components.core.paymentmethod.SepaPaymentMethod -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.sepa.SepaComponentState import com.adyen.checkout.sepa.internal.ui.model.SepaInputData import com.adyen.checkout.sepa.internal.ui.model.SepaOutputData @@ -67,7 +67,7 @@ internal class DefaultSepaDelegate( } private fun setupAnalytics(coroutineScope: CoroutineScope) { - Logger.v(TAG, "setupAnalytics") + adyenLog(AdyenLogLevel.VERBOSE) { "setupAnalytics" } coroutineScope.launch { analyticsRepository.setupAnalytics() } @@ -102,7 +102,7 @@ internal class DefaultSepaDelegate( } private fun onInputDataChanged() { - Logger.v(TAG, "onInputDataChanged") + adyenLog(AdyenLogLevel.VERBOSE) { "onInputDataChanged" } val outputData = createOutputData() _outputDataFlow.tryEmit(outputData) @@ -125,7 +125,7 @@ internal class DefaultSepaDelegate( type = SepaPaymentMethod.PAYMENT_METHOD_TYPE, checkoutAttemptId = analyticsRepository.getCheckoutAttemptId(), ownerName = outputData.ownerNameField.value, - iban = outputData.ibanNumberField.value + iban = outputData.ibanNumberField.value, ) val paymentComponentData = PaymentComponentData( paymentMethod = paymentMethod, @@ -155,8 +155,4 @@ internal class DefaultSepaDelegate( override fun onCleared() { removeObserver() } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/view/SepaView.kt b/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/view/SepaView.kt index 88356c1b8a..bdfc530abf 100644 --- a/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/view/SepaView.kt +++ b/sepa/src/main/java/com/adyen/checkout/sepa/internal/ui/view/SepaView.kt @@ -15,8 +15,8 @@ import android.view.View.OnFocusChangeListener import android.widget.LinearLayout import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.Validation -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.sepa.R import com.adyen.checkout.sepa.databinding.SepaViewBinding import com.adyen.checkout.sepa.internal.ui.SepaDelegate @@ -35,7 +35,7 @@ internal class SepaView @JvmOverloads constructor( LinearLayout( context, attrs, - defStyleAttr + defStyleAttr, ), ComponentView { @@ -82,16 +82,16 @@ internal class SepaView @JvmOverloads constructor( private fun initLocalizedStrings(localizedContext: Context) { binding.textInputLayoutHolderName.setLocalizedHintFromStyle( R.style.AdyenCheckout_Sepa_HolderNameInput, - localizedContext + localizedContext, ) binding.textInputLayoutIbanNumber.setLocalizedHintFromStyle( R.style.AdyenCheckout_Sepa_AccountNumberInput, - localizedContext + localizedContext, ) } override fun highlightValidationErrors() { - Logger.d(TAG, "highlightValidationErrors") + adyenLog(AdyenLogLevel.DEBUG) { "highlightValidationErrors" } val outputData: SepaOutputData = sepaDelegate.outputData var errorFocused = false val ownerNameValidation = outputData.ownerNameField.validation @@ -112,8 +112,4 @@ internal class SepaView @JvmOverloads constructor( } override fun getView(): View = this - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/sepa/src/test/java/com/adyen/checkout/sepa/SepaComponentTest.kt b/sepa/src/test/java/com/adyen/checkout/sepa/SepaComponentTest.kt index 2ec518c93a..82f3e3ba00 100644 --- a/sepa/src/test/java/com/adyen/checkout/sepa/SepaComponentTest.kt +++ b/sepa/src/test/java/com/adyen/checkout/sepa/SepaComponentTest.kt @@ -15,10 +15,9 @@ import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.sepa.internal.ui.SepaComponentViewType import com.adyen.checkout.sepa.internal.ui.SepaDelegate +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.invokeOnCleared import com.adyen.checkout.ui.core.internal.test.TestComponentViewType @@ -40,7 +39,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class SepaComponentTest( @Mock private val sepaDelegate: SepaDelegate, @Mock private val genericActionDelegate: GenericActionDelegate, @@ -59,9 +58,8 @@ internal class SepaComponentTest( sepaDelegate, genericActionDelegate, actionHandlingComponent, - componentEventHandler + componentEventHandler, ) - AdyenLogger.setLogLevel(Logger.NONE) } @Test diff --git a/sepa/src/test/java/com/adyen/checkout/sepa/SepaConfigurationTest.kt b/sepa/src/test/java/com/adyen/checkout/sepa/SepaConfigurationTest.kt new file mode 100644 index 0000000000..c44ffd2f6c --- /dev/null +++ b/sepa/src/test/java/com/adyen/checkout/sepa/SepaConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.sepa + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +internal class SepaConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + sepa { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getSepaConfiguration() + + val expected = SepaConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = SepaConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualSepaConfig = actual.getSepaConfiguration() + assertEquals(config.shopperLocale, actualSepaConfig?.shopperLocale) + assertEquals(config.environment, actualSepaConfig?.environment) + assertEquals(config.clientKey, actualSepaConfig?.clientKey) + assertEquals(config.amount, actualSepaConfig?.amount) + assertEquals(config.analyticsConfiguration, actualSepaConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualSepaConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/sepa/src/test/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegateTest.kt b/sepa/src/test/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegateTest.kt index 3ad2ad8370..2b7849ec65 100644 --- a/sepa/src/test/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegateTest.kt +++ b/sepa/src/test/java/com/adyen/checkout/sepa/internal/ui/DefaultSepaDelegateTest.kt @@ -10,17 +10,21 @@ package com.adyen.checkout.sepa.internal.ui import app.cash.turbine.test import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.OrderRequest import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.ui.model.ButtonComponentParamsMapper +import com.adyen.checkout.components.core.internal.ui.model.CommonComponentParamsMapper import com.adyen.checkout.components.core.paymentmethod.SepaPaymentMethod import com.adyen.checkout.core.Environment import com.adyen.checkout.sepa.SepaComponentState import com.adyen.checkout.sepa.SepaConfiguration +import com.adyen.checkout.sepa.getSepaConfiguration import com.adyen.checkout.sepa.internal.ui.model.SepaOutputData +import com.adyen.checkout.sepa.sepa import com.adyen.checkout.ui.core.internal.ui.SubmitHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -107,9 +111,7 @@ internal class DefaultSepaDelegateTest( expectedComponentStateValue: Amount?, ) = runTest { if (configurationValue != null) { - val configuration = getDefaultSepaConfigurationBuilder() - .setAmount(configurationValue) - .build() + val configuration = createCheckoutConfiguration(configurationValue) delegate = createSepaDelegate(configuration = configuration) } delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -134,9 +136,9 @@ internal class DefaultSepaDelegateTest( @Test fun `when submit button is configured to be hidden, then it should not show`() { delegate = createSepaDelegate( - configuration = getDefaultSepaConfigurationBuilder() - .setSubmitButtonVisible(false) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(false) + }, ) Assertions.assertFalse(delegate.shouldShowSubmitButton()) @@ -145,9 +147,9 @@ internal class DefaultSepaDelegateTest( @Test fun `when submit button is configured to be visible, then it should show`() { delegate = createSepaDelegate( - configuration = getDefaultSepaConfigurationBuilder() - .setSubmitButtonVisible(true) - .build() + configuration = createCheckoutConfiguration { + setSubmitButtonVisible(true) + }, ) assertTrue(delegate.shouldShowSubmitButton()) @@ -202,22 +204,34 @@ internal class DefaultSepaDelegateTest( } private fun createSepaDelegate( - configuration: SepaConfiguration = getDefaultSepaConfigurationBuilder().build(), + configuration: CheckoutConfiguration = createCheckoutConfiguration(), order: Order? = TEST_ORDER, ) = DefaultSepaDelegate( observerRepository = PaymentObserverRepository(), paymentMethod = PaymentMethod(), order = order, - componentParams = ButtonComponentParamsMapper(null, null).mapToParams(configuration, null), + componentParams = ButtonComponentParamsMapper(CommonComponentParamsMapper()).mapToParams( + checkoutConfiguration = configuration, + deviceLocale = Locale.US, + dropInOverrideParams = null, + componentSessionParams = null, + componentConfiguration = configuration.getSepaConfiguration(), + ), analyticsRepository = analyticsRepository, - submitHandler = submitHandler + submitHandler = submitHandler, ) - private fun getDefaultSepaConfigurationBuilder() = SepaConfiguration.Builder( - Locale.US, - Environment.TEST, - TEST_CLIENT_KEY - ) + private fun createCheckoutConfiguration( + amount: Amount? = null, + configuration: SepaConfiguration.Builder.() -> Unit = {}, + ) = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = amount, + ) { + sepa(configuration) + } companion object { private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSession.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSession.kt index 13deb666a7..b702fe40b8 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSession.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSession.kt @@ -8,8 +8,11 @@ package com.adyen.checkout.sessions.core +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.core.Environment /** * A class holding the data required to launch Drop-in or a component with the sessions flow. @@ -18,10 +21,20 @@ import com.adyen.checkout.components.core.PaymentMethod data class CheckoutSession( val sessionSetupResponse: SessionSetupResponse, val order: Order?, + val environment: Environment, + val clientKey: String, ) { fun getPaymentMethod(paymentMethodType: String): PaymentMethod? { return sessionSetupResponse.paymentMethodsApiResponse?.paymentMethods.orEmpty().firstOrNull { it.type == paymentMethodType } } + + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun getConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + environment = environment, + clientKey = clientKey, + ) + } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSessionProvider.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSessionProvider.kt index 2c5439eb7c..8d939ce359 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSessionProvider.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/CheckoutSessionProvider.kt @@ -10,6 +10,7 @@ package com.adyen.checkout.sessions.core import com.adyen.checkout.components.core.Order import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.sessions.core.internal.CheckoutSessionInitializer @@ -32,7 +33,33 @@ object CheckoutSessionProvider { configuration: Configuration, order: Order? = null, ): CheckoutSessionResult { - return CheckoutSessionInitializer(sessionModel, configuration, order).setupSession(null) + return createSession( + sessionModel = sessionModel, + environment = configuration.environment, + clientKey = configuration.clientKey, + order = order, + ) + } + + /** + * Allows creating a [CheckoutSession] from the response of the /sessions endpoint. + * This is a suspend function that executes a network call on the IO thread. + * + * @param sessionModel The deserialized JSON response of the /sessions API call. You can use + * [SessionModel.SERIALIZER] to deserialize this JSON. + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + * @param order An [Order] in case of an ongoing partial payment flow. + * + * @return The result of the API call. + */ + suspend fun createSession( + sessionModel: SessionModel, + environment: Environment, + clientKey: String, + order: Order? = null, + ): CheckoutSessionResult { + return CheckoutSessionInitializer(sessionModel, environment, clientKey, order).setupSession(null) } /** @@ -52,6 +79,32 @@ object CheckoutSessionProvider { suspend fun createSession( sessionPaymentResult: SessionPaymentResult, configuration: Configuration, + ): CheckoutSessionResult { + return createSession( + sessionPaymentResult = sessionPaymentResult, + environment = configuration.environment, + clientKey = configuration.clientKey, + ) + } + + /** + * Only to be used for initializing a component for partial payment flow. + * + * Allows creating a [CheckoutSession] from the response of the /sessions endpoint. + * This is a suspend function that executes a network call on the IO thread. + * + * @param sessionPaymentResult The [SessionPaymentResult] object to initialize the session. You will get this + * object via [com.adyen.checkout.giftcard.SessionsGiftCardComponentCallback.onPartialPayment] callback after + * a partial payment has been done. + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + * + * @return The result of the API call. + */ + suspend fun createSession( + sessionPaymentResult: SessionPaymentResult, + environment: Environment, + clientKey: String, ): CheckoutSessionResult { if (sessionPaymentResult.sessionId == null) { throw CheckoutException("sessionId must not be null to create a session.") @@ -60,7 +113,7 @@ object CheckoutSessionProvider { val order = sessionPaymentResult.order?.let { orderResponse -> Order(orderResponse.pspReference, orderResponse.orderData) } - return CheckoutSessionInitializer(sessionModel, configuration, order) + return CheckoutSessionInitializer(sessionModel, environment, clientKey, order) .setupSession(sessionPaymentResult.order?.remainingAmount) } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionComponentCallback.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionComponentCallback.kt index d8ba332fca..00e9b69b78 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionComponentCallback.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionComponentCallback.kt @@ -15,6 +15,7 @@ import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.BaseComponentCallback import com.adyen.checkout.components.core.internal.PaymentComponent +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.MethodNotImplementedException import org.json.JSONObject @@ -118,4 +119,18 @@ interface SessionComponentCallback> : BaseComponent * UI. */ fun onLoading(isLoading: Boolean) = Unit + + /** + * Should be overridden to support runtime permissions for components. + * Runtime permission should be requested and communicated back through the callback. + * If not overridden, [PermissionHandlerCallback.onPermissionRequestNotHandled] will be invoked, which will show an + * error message. + * + * @param requiredPermission Required runtime permission. + * @param permissionCallback Callback to be used when passing permission result. + */ + fun onPermissionRequest(requiredPermission: String, permissionCallback: PermissionHandlerCallback) { + // To be optionally overridden + permissionCallback.onPermissionRequestNotHandled(requiredPermission) + } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionPaymentResult.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionPaymentResult.kt index 9272091271..badbeacaed 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionPaymentResult.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionPaymentResult.kt @@ -13,8 +13,14 @@ import com.adyen.checkout.components.core.OrderResponse import kotlinx.parcelize.Parcelize /** - * The final result of a payment using the sessions flow. - * You can use the [sessionId] and [sessionResult] to get the result of the payment session on your server. + * The result of a payment using the sessions flow. + * + * @param sessionId A unique identifier of the session. + * @param sessionResult You can forward this alongside [sessionId] to your server to fetch the result of the payment. + * @param sessionData The payment session data. + * @param resultCode The result code of the payment. For more information, see + * [Result codes](https://docs.adyen.com/online-payments/build-your-integration/payment-result-codes/). + * @param order An order, only applicable in case of an ongoing partial payment flow. */ @Parcelize data class SessionPaymentResult( diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt index ecb2a61334..336950b982 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupConfiguration.kt @@ -19,13 +19,15 @@ import org.json.JSONObject data class SessionSetupConfiguration( val enableStoreDetails: Boolean? = null, val showInstallmentAmount: Boolean = false, - val installmentOptions: Map? = null + val installmentOptions: Map? = null, + val showRemovePaymentMethodButton: Boolean = false, ) : ModelObject() { companion object { private const val ENABLE_STORE_DETAILS = "enableStoreDetails" private const val SHOW_INSTALLMENT_AMOUNT = "showInstallmentAmount" private const val INSTALLMENT_OPTIONS = "installmentOptions" + private const val SHOW_REMOVE_PAYMENT_METHOD_BUTTON = "showRemovePaymentMethodButton" @JvmField val SERIALIZER: Serializer = object : Serializer { @@ -36,8 +38,9 @@ data class SessionSetupConfiguration( putOpt(SHOW_INSTALLMENT_AMOUNT, modelObject.showInstallmentAmount) putOpt( INSTALLMENT_OPTIONS, - modelObject.installmentOptions?.let { JSONObject(it) } + modelObject.installmentOptions?.let { JSONObject(it) }, ) + putOpt(SHOW_REMOVE_PAYMENT_METHOD_BUTTON, modelObject.showRemovePaymentMethodButton) } } catch (e: JSONException) { throw ModelSerializationException(SessionSetupConfiguration::class.java, e) @@ -50,7 +53,8 @@ data class SessionSetupConfiguration( enableStoreDetails = jsonObject.optBoolean(ENABLE_STORE_DETAILS), showInstallmentAmount = jsonObject.optBoolean(SHOW_INSTALLMENT_AMOUNT), installmentOptions = jsonObject.optJSONObject(INSTALLMENT_OPTIONS) - ?.jsonToMap(SessionSetupInstallmentOptions.SERIALIZER) + ?.jsonToMap(SessionSetupInstallmentOptions.SERIALIZER), + showRemovePaymentMethodButton = jsonObject.optBoolean(SHOW_REMOVE_PAYMENT_METHOD_BUTTON), ) } catch (e: JSONException) { throw ModelSerializationException(SessionSetupConfiguration::class.java, e) diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupResponse.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupResponse.kt index b8a11c10a5..ed32a60092 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupResponse.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/SessionSetupResponse.kt @@ -25,7 +25,8 @@ data class SessionSetupResponse( val expiresAt: String, val paymentMethodsApiResponse: PaymentMethodsApiResponse?, val returnUrl: String?, - val configuration: SessionSetupConfiguration? + val configuration: SessionSetupConfiguration?, + val shopperLocale: String?, ) : ModelObject() { companion object { @@ -36,6 +37,7 @@ data class SessionSetupResponse( private const val PAYMENT_METHODS = "paymentMethods" private const val RETURN_URL = "returnUrl" private const val CONFIGURATION = "configuration" + private const val SHOPPER_LOCALE = "shopperLocale" @JvmField val SERIALIZER: Serializer = object : Serializer { @@ -58,6 +60,7 @@ data class SessionSetupResponse( CONFIGURATION, ModelUtils.serializeOpt(modelObject.configuration, SessionSetupConfiguration.SERIALIZER) ) + jsonObject.putOpt(SHOPPER_LOCALE, modelObject.shopperLocale) } catch (e: JSONException) { throw ModelSerializationException(SessionSetupResponse::class.java, e) } @@ -79,7 +82,8 @@ data class SessionSetupResponse( configuration = ModelUtils.deserializeOpt( jsonObject.optJSONObject(CONFIGURATION), SessionSetupConfiguration.SERIALIZER - ) + ), + shopperLocale = jsonObject.optString(SHOPPER_LOCALE), ) } catch (e: JSONException) { throw ModelSerializationException(SessionSetupResponse::class.java, e) diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/CheckoutSessionInitializer.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/CheckoutSessionInitializer.kt index 05bbafd7e4..820383414d 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/CheckoutSessionInitializer.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/CheckoutSessionInitializer.kt @@ -10,7 +10,7 @@ package com.adyen.checkout.sessions.core.internal import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.Order -import com.adyen.checkout.components.core.internal.Configuration +import com.adyen.checkout.core.Environment import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.internal.data.api.HttpClientFactory import com.adyen.checkout.sessions.core.CheckoutSession @@ -18,21 +18,24 @@ import com.adyen.checkout.sessions.core.CheckoutSessionResult import com.adyen.checkout.sessions.core.SessionModel import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository import com.adyen.checkout.sessions.core.internal.data.api.SessionService +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext internal class CheckoutSessionInitializer( private val sessionModel: SessionModel, - configuration: Configuration, + private val environment: Environment, + private val clientKey: String, private val order: Order?, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { - private val httpClient = HttpClientFactory.getHttpClient(configuration.environment) + private val httpClient = HttpClientFactory.getHttpClient(environment) private val sessionService = SessionService(httpClient) - private val sessionRepository = SessionRepository(sessionService, configuration.clientKey) + private val sessionRepository = SessionRepository(sessionService, clientKey) // TODO: Once Backend provides the correct amount in the SessionSetupResponse use that in SessionDetails instead of // override Amount - suspend fun setupSession(overrideAmount: Amount?): CheckoutSessionResult = withContext(Dispatchers.IO) { + suspend fun setupSession(overrideAmount: Amount?): CheckoutSessionResult = withContext(coroutineDispatcher) { sessionRepository.setupSession( sessionModel = sessionModel, order = order, @@ -41,13 +44,15 @@ internal class CheckoutSessionInitializer( return@withContext CheckoutSessionResult.Success( CheckoutSession( sessionSetupResponse.copy(amount = overrideAmount ?: sessionSetupResponse.amount), - order - ) + order, + environment, + clientKey, + ), ) }, onFailure = { return@withContext CheckoutSessionResult.Error(CheckoutException("Failed to fetch session", it)) - } + }, ) } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionCallResult.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionCallResult.kt index c81b0c1c04..52295b7ace 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionCallResult.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionCallResult.kt @@ -52,6 +52,11 @@ interface SessionCallResult { object TakenOver : CancelOrder() } + sealed class RemoveStoredPaymentMethod : SessionCallResult { + data object Successful : RemoveStoredPaymentMethod() + data class Error(val throwable: Throwable) : RemoveStoredPaymentMethod() + } + sealed class UpdatePaymentMethods : SessionCallResult { data class Successful( val paymentMethods: PaymentMethodsApiResponse, diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandler.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandler.kt index f18f1c52e7..7b98ad1ecf 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandler.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandler.kt @@ -15,9 +15,10 @@ import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.internal.BaseComponentCallback import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.PaymentComponentEvent +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.SessionPaymentResult import kotlinx.coroutines.CoroutineScope @@ -45,7 +46,7 @@ class SessionComponentEventHandler>( } private fun updateSessionData(sessionData: String) { - Logger.v(TAG, "Updating session data - $sessionData") + adyenLog(AdyenLogLevel.VERBOSE) { "Updating session data - $sessionData" } sessionSavedStateHandleContainer.updateSessionData(sessionData) } @@ -53,11 +54,17 @@ class SessionComponentEventHandler>( @Suppress("UNCHECKED_CAST") val sessionComponentCallback = componentCallback as? SessionComponentCallback ?: throw CheckoutException("Callback must be type of ${SessionComponentCallback::class.java.canonicalName}") - Logger.v(TAG, "Event received $event") + adyenLog(AdyenLogLevel.VERBOSE) { "Event received $event" } when (event) { is PaymentComponentEvent.ActionDetails -> onDetailsCallRequested(event.data, sessionComponentCallback) is PaymentComponentEvent.Error -> onComponentError(event.error, sessionComponentCallback) is PaymentComponentEvent.StateChanged -> onState(event.state, sessionComponentCallback) + is PaymentComponentEvent.PermissionRequest -> onPermissionRequest( + event.requiredPermission, + event.permissionCallback, + sessionComponentCallback, + ) + is PaymentComponentEvent.Submit -> onPaymentsCallRequested(event.state, sessionComponentCallback) } } @@ -77,13 +84,15 @@ class SessionComponentEventHandler>( is SessionCallResult.Payments.Action -> { sessionComponentCallback.onAction(result.action) } + is SessionCallResult.Payments.Error -> onSessionError(result.throwable, sessionComponentCallback) is SessionCallResult.Payments.Finished -> onFinished(result.result, sessionComponentCallback) is SessionCallResult.Payments.NotFullyPaidOrder -> onFinished(result.result, sessionComponentCallback) is SessionCallResult.Payments.RefusedPartialPayment -> onFinished( result.result, - sessionComponentCallback + sessionComponentCallback, ) + is SessionCallResult.Payments.TakenOver -> { setFlowTakenOver() } @@ -99,13 +108,14 @@ class SessionComponentEventHandler>( val result = sessionInteractor.onDetailsCallRequested( actionComponentData, sessionComponentCallback::onAdditionalDetails, - sessionComponentCallback::onAdditionalDetails.name + sessionComponentCallback::onAdditionalDetails.name, ) when (result) { is SessionCallResult.Details.Action -> { sessionComponentCallback.onAction(result.action) } + is SessionCallResult.Details.Error -> onSessionError(result.throwable, sessionComponentCallback) is SessionCallResult.Details.Finished -> onFinished(result.result, sessionComponentCallback) SessionCallResult.Details.TakenOver -> { @@ -130,6 +140,14 @@ class SessionComponentEventHandler>( sessionComponentCallback.onStateChanged(state) } + private fun onPermissionRequest( + requiredPermission: String, + permissionCallback: PermissionHandlerCallback, + sessionComponentCallback: SessionComponentCallback + ) { + sessionComponentCallback.onPermissionRequest(requiredPermission, permissionCallback) + } + private fun onComponentError(error: ComponentError, sessionComponentCallback: SessionComponentCallback) { sessionComponentCallback.onError(error) } @@ -137,27 +155,23 @@ class SessionComponentEventHandler>( private fun onSessionError(throwable: Throwable, sessionComponentCallback: SessionComponentCallback) { sessionComponentCallback.onError( ComponentError( - CheckoutException(throwable.message.orEmpty(), throwable) - ) + CheckoutException(throwable.message.orEmpty(), throwable), + ), ) } private fun onFinished(result: SessionPaymentResult, sessionComponentCallback: SessionComponentCallback) { - Logger.d(TAG, "Finished: ${result.resultCode}") + adyenLog(AdyenLogLevel.DEBUG) { "Finished: ${result.resultCode}" } sessionComponentCallback.onFinished(result) } private fun setFlowTakenOver() { if (sessionSavedStateHandleContainer.isFlowTakenOver == true) return sessionSavedStateHandleContainer.isFlowTakenOver = true - Logger.i(TAG, "Flow was taken over.") + adyenLog(AdyenLogLevel.INFO) { "Flow was taken over." } } override fun onCleared() { _coroutineScope = null } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionInteractor.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionInteractor.kt index 5f40f5869f..9cff52c7be 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionInteractor.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionInteractor.kt @@ -53,7 +53,7 @@ class SessionInteractor( merchantCall = { merchantCall(paymentComponentState) }, internalCall = { makePaymentsCallInternal(paymentComponentState) }, merchantMethodName = merchantCallName, - takenOverFactory = { SessionCallResult.Payments.TakenOver } + takenOverFactory = { SessionCallResult.Payments.TakenOver }, ) } @@ -69,6 +69,7 @@ class SessionInteractor( return when { response.isRefusedInPartialPaymentFlow() -> SessionCallResult.Payments.RefusedPartialPayment(response.mapToSessionPaymentResult()) + action != null -> SessionCallResult.Payments.Action(action) response.order.isNonFullyPaid() -> onNonFullyPaidOrder(response) else -> SessionCallResult.Payments.Finished(response.mapToSessionPaymentResult()) @@ -76,7 +77,7 @@ class SessionInteractor( }, onFailure = { return SessionCallResult.Payments.Error(throwable = it) - } + }, ) } @@ -96,7 +97,7 @@ class SessionInteractor( merchantCall = { merchantCall(actionComponentData) }, internalCall = { makeDetailsCallInternal(actionComponentData) }, merchantMethodName = merchantCallName, - takenOverFactory = { SessionCallResult.Details.TakenOver } + takenOverFactory = { SessionCallResult.Details.TakenOver }, ) } @@ -113,7 +114,7 @@ class SessionInteractor( }, onFailure = { return SessionCallResult.Details.Error(throwable = it) - } + }, ) } @@ -132,23 +133,17 @@ class SessionInteractor( private suspend fun makeCheckBalanceCallInternal( paymentComponentState: PaymentComponentState<*> - ): SessionCallResult.Balance { - sessionRepository.checkBalance(sessionModel, paymentComponentState) - .fold( - onSuccess = { response -> - updateSessionData(response.sessionData) - return if (response.balance.value <= 0) { - SessionCallResult.Balance.Error(CheckoutException("Not enough balance")) - } else { - val balanceResult = BalanceResult(response.balance, response.transactionLimit) - SessionCallResult.Balance.Successful(balanceResult) - } - }, - onFailure = { - return SessionCallResult.Balance.Error(throwable = it) - } - ) - } + ) = sessionRepository.checkBalance(sessionModel, paymentComponentState) + .fold( + onSuccess = { response -> + updateSessionData(response.sessionData) + val balanceResult = BalanceResult(response.balance, response.transactionLimit) + SessionCallResult.Balance.Successful(balanceResult) + }, + onFailure = { + SessionCallResult.Balance.Error(throwable = it) + }, + ) suspend fun createOrder( merchantCall: () -> Boolean, @@ -158,7 +153,7 @@ class SessionInteractor( merchantCall = { merchantCall() }, internalCall = { makeCreateOrderInternal() }, merchantMethodName = merchantCallName, - takenOverFactory = { SessionCallResult.CreateOrder.TakenOver } + takenOverFactory = { SessionCallResult.CreateOrder.TakenOver }, ) } @@ -178,7 +173,7 @@ class SessionInteractor( }, onFailure = { return SessionCallResult.CreateOrder.Error(throwable = it) - } + }, ) } @@ -191,7 +186,7 @@ class SessionInteractor( merchantCall = { merchantCall(order) }, internalCall = { makeCancelOrderCallInternal(order) }, merchantMethodName = merchantCallName, - takenOverFactory = { SessionCallResult.CancelOrder.TakenOver } + takenOverFactory = { SessionCallResult.CancelOrder.TakenOver }, ) } @@ -207,7 +202,7 @@ class SessionInteractor( }, onFailure = { return SessionCallResult.CancelOrder.Error(throwable = it) - } + }, ) } @@ -215,7 +210,7 @@ class SessionInteractor( val orderRequest = order?.let { OrderRequest( pspReference = order.pspReference, - orderData = order.orderData + orderData = order.orderData, ) } @@ -230,14 +225,28 @@ class SessionInteractor( } else { SessionCallResult.UpdatePaymentMethods.Error( throwable = CheckoutException( - errorMessage = "Payment methods should not be null" - ) + errorMessage = "Payment methods should not be null", + ), ) } }, onFailure = { return SessionCallResult.UpdatePaymentMethods.Error(throwable = it) - } + }, + ) + } + + suspend fun removeStoredPaymentMethod(storedPaymentMethodId: String): SessionCallResult.RemoveStoredPaymentMethod { + sessionRepository.disableToken(sessionModel, storedPaymentMethodId) + .fold( + onSuccess = { + updateSessionData(it.sessionData) + + return SessionCallResult.RemoveStoredPaymentMethod.Successful + }, + onFailure = { + return SessionCallResult.RemoveStoredPaymentMethod.Error(throwable = it) + }, ) } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionSavedStateHandleContainer.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionSavedStateHandleContainer.kt index 9fb111f6d2..180270aee3 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionSavedStateHandleContainer.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/SessionSavedStateHandleContainer.kt @@ -29,7 +29,7 @@ class SessionSavedStateHandleContainer( init { if (sessionDetails == null) { - sessionDetails = checkoutSession.sessionSetupResponse.mapToDetails() + sessionDetails = checkoutSession.mapToDetails() } if (isFlowTakenOver == null) { isFlowTakenOver = false diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionRepository.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionRepository.kt index 4867046e6f..5f1dcf5c86 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionRepository.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionRepository.kt @@ -23,6 +23,8 @@ import com.adyen.checkout.sessions.core.internal.data.model.SessionCancelOrderRe import com.adyen.checkout.sessions.core.internal.data.model.SessionCancelOrderResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionDetailsRequest import com.adyen.checkout.sessions.core.internal.data.model.SessionDetailsResponse +import com.adyen.checkout.sessions.core.internal.data.model.SessionDisableTokenRequest +import com.adyen.checkout.sessions.core.internal.data.model.SessionDisableTokenResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionOrderRequest import com.adyen.checkout.sessions.core.internal.data.model.SessionOrderResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionPaymentsRequest @@ -43,7 +45,7 @@ class SessionRepository( sessionService.setupSession( request = request, sessionId = sessionModel.id, - clientKey = clientKey + clientKey = clientKey, ) } @@ -55,7 +57,7 @@ class SessionRepository( sessionService.submitPayment( request = request, sessionId = sessionModel.id, - clientKey = clientKey + clientKey = clientKey, ) } @@ -66,12 +68,12 @@ class SessionRepository( val request = SessionDetailsRequest( sessionData = sessionModel.sessionData.orEmpty(), paymentData = actionComponentData.paymentData, - details = actionComponentData.details + details = actionComponentData.details, ) sessionService.submitDetails( request = request, sessionId = sessionModel.id, - clientKey = clientKey + clientKey = clientKey, ) } @@ -82,12 +84,12 @@ class SessionRepository( val request = SessionBalanceRequest( sessionModel.sessionData.orEmpty(), paymentComponentState.data.paymentMethod, - paymentComponentState.data.amount + paymentComponentState.data.amount, ) sessionService.checkBalance( request = request, sessionId = sessionModel.id, - clientKey = clientKey + clientKey = clientKey, ) } @@ -96,7 +98,7 @@ class SessionRepository( sessionService.createOrder( request = request, sessionId = sessionModel.id, - clientKey = clientKey + clientKey = clientKey, ) } @@ -108,7 +110,19 @@ class SessionRepository( sessionService.cancelOrder( request = request, sessionId = sessionModel.id, - clientKey = clientKey + clientKey = clientKey, + ) + } + + suspend fun disableToken( + sessionModel: SessionModel, + storedPaymentMethodId: String, + ): Result = runSuspendCatching { + val request = SessionDisableTokenRequest(sessionModel.sessionData.orEmpty(), storedPaymentMethodId) + sessionService.disableToken( + request = request, + sessionId = sessionModel.id, + clientKey = clientKey, ) } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionService.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionService.kt index 96abca4e36..81e04391a9 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionService.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/api/SessionService.kt @@ -18,24 +18,28 @@ import com.adyen.checkout.sessions.core.internal.data.model.SessionCancelOrderRe import com.adyen.checkout.sessions.core.internal.data.model.SessionCancelOrderResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionDetailsRequest import com.adyen.checkout.sessions.core.internal.data.model.SessionDetailsResponse +import com.adyen.checkout.sessions.core.internal.data.model.SessionDisableTokenRequest +import com.adyen.checkout.sessions.core.internal.data.model.SessionDisableTokenResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionOrderRequest import com.adyen.checkout.sessions.core.internal.data.model.SessionOrderResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionPaymentsRequest import com.adyen.checkout.sessions.core.internal.data.model.SessionPaymentsResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionSetupRequest +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class SessionService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { suspend fun setupSession( request: SessionSetupRequest, sessionId: String, clientKey: String, - ): SessionSetupResponse = withContext(Dispatchers.IO) { + ): SessionSetupResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/sessions/$sessionId/setup", queryParameters = mapOf("clientKey" to clientKey), @@ -49,7 +53,7 @@ class SessionService( request: SessionPaymentsRequest, sessionId: String, clientKey: String, - ): SessionPaymentsResponse = withContext(Dispatchers.IO) { + ): SessionPaymentsResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/sessions/$sessionId/payments", queryParameters = mapOf("clientKey" to clientKey), @@ -63,7 +67,7 @@ class SessionService( request: SessionDetailsRequest, sessionId: String, clientKey: String, - ): SessionDetailsResponse = withContext(Dispatchers.IO) { + ): SessionDetailsResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/sessions/$sessionId/paymentDetails", queryParameters = mapOf("clientKey" to clientKey), @@ -77,7 +81,7 @@ class SessionService( request: SessionBalanceRequest, sessionId: String, clientKey: String, - ): SessionBalanceResponse = withContext(Dispatchers.IO) { + ): SessionBalanceResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/sessions/$sessionId/paymentMethodBalance", queryParameters = mapOf("clientKey" to clientKey), @@ -91,7 +95,7 @@ class SessionService( request: SessionOrderRequest, sessionId: String, clientKey: String, - ): SessionOrderResponse = withContext(Dispatchers.IO) { + ): SessionOrderResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/sessions/$sessionId/orders", queryParameters = mapOf("clientKey" to clientKey), @@ -105,7 +109,7 @@ class SessionService( request: SessionCancelOrderRequest, sessionId: String, clientKey: String, - ): SessionCancelOrderResponse = withContext(Dispatchers.IO) { + ): SessionCancelOrderResponse = withContext(coroutineDispatcher) { httpClient.post( path = "v1/sessions/$sessionId/orders/cancel", queryParameters = mapOf("clientKey" to clientKey), @@ -114,4 +118,18 @@ class SessionService( responseSerializer = SessionCancelOrderResponse.SERIALIZER, ) } + + suspend fun disableToken( + request: SessionDisableTokenRequest, + sessionId: String, + clientKey: String, + ): SessionDisableTokenResponse = withContext(coroutineDispatcher) { + httpClient.post( + path = "v1/sessions/$sessionId/disableToken", + queryParameters = mapOf("clientKey" to clientKey), + body = request, + requestSerializer = SessionDisableTokenRequest.SERIALIZER, + responseSerializer = SessionDisableTokenResponse.SERIALIZER, + ) + } } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDetails.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDetails.kt index f7e059e2b5..cc911b6df3 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDetails.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDetails.kt @@ -11,11 +11,10 @@ package com.adyen.checkout.sessions.core.internal.data.model import android.os.Parcelable import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.Amount -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.core.Environment +import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionModel import com.adyen.checkout.sessions.core.SessionSetupConfiguration -import com.adyen.checkout.sessions.core.SessionSetupResponse -import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory import kotlinx.parcelize.Parcelize @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @@ -26,18 +25,24 @@ data class SessionDetails( val amount: Amount?, val expiresAt: String, val returnUrl: String?, - val sessionSetupConfiguration: SessionSetupConfiguration? + val sessionSetupConfiguration: SessionSetupConfiguration?, + val shopperLocale: String?, + val environment: Environment, + val clientKey: String, ) : Parcelable @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -fun SessionSetupResponse.mapToDetails(): SessionDetails { +fun CheckoutSession.mapToDetails(): SessionDetails { return SessionDetails( - id = id, - sessionData = sessionData, - amount = amount, - expiresAt = expiresAt, - returnUrl = returnUrl, - sessionSetupConfiguration = configuration + environment = environment, + clientKey = clientKey, + id = sessionSetupResponse.id, + sessionData = sessionSetupResponse.sessionData, + amount = sessionSetupResponse.amount, + expiresAt = sessionSetupResponse.expiresAt, + returnUrl = sessionSetupResponse.returnUrl, + sessionSetupConfiguration = sessionSetupResponse.configuration, + shopperLocale = sessionSetupResponse.shopperLocale, ) } @@ -48,9 +53,3 @@ fun SessionDetails.mapToModel(): SessionModel { sessionData = sessionData, ) } - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -fun SessionDetails.mapToParams(amount: Amount?): SessionParams { - // TODO: Once Backend provides the correct amount in the SessionSetupResponse use that in SessionDetails - return SessionParamsFactory.create(this.copy(amount = amount)) -} diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDisableTokenRequest.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDisableTokenRequest.kt new file mode 100644 index 0000000000..cce3a5b34b --- /dev/null +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDisableTokenRequest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 15/2/2024. + */ + +package com.adyen.checkout.sessions.core.internal.data.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.data.model.ModelObject +import kotlinx.parcelize.Parcelize +import org.json.JSONException +import org.json.JSONObject + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@Parcelize +data class SessionDisableTokenRequest( + val sessionData: String, + val storedPaymentMethodId: String +) : ModelObject() { + companion object { + private const val SESSION_DATA = "sessionData" + private const val STORED_PAYMENT_METHOD_ID = "storedPaymentMethodId" + + @JvmField + val SERIALIZER: Serializer = object : Serializer { + override fun serialize(modelObject: SessionDisableTokenRequest): JSONObject { + val jsonObject = JSONObject() + try { + jsonObject.putOpt(SESSION_DATA, modelObject.sessionData) + jsonObject.putOpt(STORED_PAYMENT_METHOD_ID, modelObject.storedPaymentMethodId) + } catch (e: JSONException) { + throw ModelSerializationException(SessionDisableTokenRequest::class.java, e) + } + return jsonObject + } + + override fun deserialize(jsonObject: JSONObject): SessionDisableTokenRequest { + return try { + SessionDisableTokenRequest( + sessionData = jsonObject.optString(SESSION_DATA), + storedPaymentMethodId = jsonObject.optString(STORED_PAYMENT_METHOD_ID), + ) + } catch (e: JSONException) { + throw ModelSerializationException(SessionDisableTokenRequest::class.java, e) + } + } + } + } +} diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDisableTokenResponse.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDisableTokenResponse.kt new file mode 100644 index 0000000000..e0fe3c37d1 --- /dev/null +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/data/model/SessionDisableTokenResponse.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 15/2/2024. + */ + +package com.adyen.checkout.sessions.core.internal.data.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.core.exception.ModelSerializationException +import com.adyen.checkout.core.internal.data.model.ModelObject +import kotlinx.parcelize.Parcelize +import org.json.JSONException +import org.json.JSONObject + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +@Parcelize +data class SessionDisableTokenResponse( + val sessionData: String, +) : ModelObject() { + companion object { + private const val SESSION_DATA = "sessionData" + + @JvmField + val SERIALIZER: Serializer = object : Serializer { + override fun serialize(modelObject: SessionDisableTokenResponse): JSONObject { + val jsonObject = JSONObject() + try { + jsonObject.putOpt(SESSION_DATA, modelObject.sessionData) + } catch (e: JSONException) { + throw ModelSerializationException(SessionDisableTokenResponse::class.java, e) + } + return jsonObject + } + + override fun deserialize(jsonObject: JSONObject): SessionDisableTokenResponse { + return try { + SessionDisableTokenResponse( + sessionData = jsonObject.optString(SESSION_DATA), + ) + } catch (e: JSONException) { + throw ModelSerializationException(SessionDisableTokenResponse::class.java, e) + } + } + } + } +} diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionPaymentComponentProvider.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionPaymentComponentProvider.kt index 9d5b3a1241..0950b917cc 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionPaymentComponentProvider.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionPaymentComponentProvider.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.PaymentMethod import com.adyen.checkout.components.core.internal.Configuration @@ -29,6 +30,184 @@ interface SessionPaymentComponentProvider< ComponentCallbackT : SessionComponentCallback > { + //region CheckoutConfiguration + + /** + * Get a [PaymentComponent] with a checkout session. You only need to integrate with the /sessions endpoint to + * create a session and the component will automatically handle the rest of the payment flow. + * + * @param fragment The Fragment to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + fragment: Fragment, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = fragment, + viewModelStoreOwner = fragment, + lifecycleOwner = fragment.viewLifecycleOwner, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = fragment.requireApplication(), + componentCallback = componentCallback, + key = key, + ) + } + + /** + * Get a [PaymentComponent] with a checkout session. You only need to integrate with the /sessions endpoint to + * create a session and the component will automatically handle the rest of the payment flow. + * + * @param fragment The Fragment to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + fragment: Fragment, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + fragment = fragment, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutSession.getConfiguration(), + componentCallback = componentCallback, + ) + } + + /** + * Get a [PaymentComponent] with a checkout session. You only need to integrate with the /sessions endpoint to + * create a session and the component will automatically handle the rest of the payment flow. + * + * @param activity The Activity to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + activity: ComponentActivity, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = activity, + viewModelStoreOwner = activity, + lifecycleOwner = activity, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = activity.application, + componentCallback = componentCallback, + key = key, + ) + } + + /** + * Get a [PaymentComponent] with a checkout session. You only need to integrate with the /sessions endpoint to + * create a session and the component will automatically handle the rest of the payment flow. + * + * @param activity The Activity to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + activity: ComponentActivity, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + activity = activity, + checkoutSession = checkoutSession, + paymentMethod = paymentMethod, + checkoutConfiguration = checkoutSession.getConfiguration(), + componentCallback = componentCallback, + ) + } + + /** + * Get a [PaymentComponent] with a checkout session. You only need to integrate with the /sessions endpoint to + * create a session and the component will automatically handle the rest of the payment flow. + * + * @param savedStateRegistryOwner The owner of the SavedStateRegistry, normally an Activity or Fragment. + * @param viewModelStoreOwner A scope that owns ViewModelStore, normally an Activity or Fragment. + * @param lifecycleOwner The lifecycle owner, normally an Activity or Fragment. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param paymentMethod The corresponding [PaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param application Your main application class. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + paymentMethod: PaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallbackT, + key: String?, + ): ComponentT + + //endregion + + //region Payment method specific configuration + /** * Get a [PaymentComponent] with a checkout session. You only need to integrate with the /sessions endpoint to * create a session and the component will automatically handle the rest of the payment flow. @@ -136,4 +315,6 @@ interface SessionPaymentComponentProvider< componentCallback: ComponentCallbackT, key: String?, ): ComponentT + + //endregion } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionStoredPaymentComponentProvider.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionStoredPaymentComponentProvider.kt index 321b3cf944..d12a2c9442 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionStoredPaymentComponentProvider.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/provider/SessionStoredPaymentComponentProvider.kt @@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModelStoreOwner import androidx.savedstate.SavedStateRegistryOwner +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.StoredPaymentMethod import com.adyen.checkout.components.core.internal.Configuration @@ -29,6 +30,189 @@ interface SessionStoredPaymentComponentProvider< ComponentCallbackT : SessionComponentCallback > { + //region CheckoutConfiguration + + /** + * Get a [PaymentComponent] with a stored payment method and a checkout session. You only need to integrate with + * the /sessions endpoint to create a session and the component will automatically handle the rest of the payment + * flow. + * + * @param fragment The Fragment to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + fragment: Fragment, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = fragment, + viewModelStoreOwner = fragment, + lifecycleOwner = fragment.viewLifecycleOwner, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = fragment.requireApplication(), + componentCallback = componentCallback, + key = key, + ) + } + + /** + * Get a [PaymentComponent] with a stored payment method and a checkout session. You only need to integrate with + * the /sessions endpoint to create a session and the component will automatically handle the rest of the payment + * flow. + * + * @param fragment The Fragment to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + fragment: Fragment, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + fragment = fragment, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutSession.getConfiguration(), + componentCallback = componentCallback, + ) + } + + /** + * Get a [PaymentComponent] with a stored payment method and a checkout session. You only need to integrate with + * the /sessions endpoint to create a session and the component will automatically handle the rest of the payment + * flow. + * + * @param activity The Activity to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + activity: ComponentActivity, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + savedStateRegistryOwner = activity, + viewModelStoreOwner = activity, + lifecycleOwner = activity, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutConfiguration, + application = activity.application, + componentCallback = componentCallback, + key = key, + ) + } + + /** + * Get a [PaymentComponent] with a stored payment method and a checkout session. You only need to integrate with + * the /sessions endpoint to create a session and the component will automatically handle the rest of the payment + * flow. + * + * @param activity The Activity to associate the lifecycle. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + activity: ComponentActivity, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + componentCallback: ComponentCallbackT, + key: String? = null, + ): ComponentT { + return get( + activity = activity, + checkoutSession = checkoutSession, + storedPaymentMethod = storedPaymentMethod, + checkoutConfiguration = checkoutSession.getConfiguration(), + componentCallback = componentCallback, + ) + } + + /** + * Get a [PaymentComponent] with a stored payment method and a checkout session. You only need to integrate with + * the /sessions endpoint to create a session and the component will automatically handle the rest of the payment + * flow. + * + * @param savedStateRegistryOwner The owner of the SavedStateRegistry, normally an Activity or Fragment. + * @param viewModelStoreOwner A scope that owns ViewModelStore, normally an Activity or Fragment. + * @param lifecycleOwner The lifecycle owner, normally an Activity or Fragment. + * @param checkoutSession The [CheckoutSession] object to launch this component. + * @param storedPaymentMethod The corresponding [StoredPaymentMethod] object. + * @param checkoutConfiguration The [CheckoutConfiguration]. + * @param application Your main application class. + * @param componentCallback The callback to handle events from the [PaymentComponent]. + * @param key The key to use to identify the [PaymentComponent]. + * + * NOTE: By default only one [PaymentComponent] will be created per lifecycle. Use [key] in case you need to + * instantiate multiple [PaymentComponent]s in the same lifecycle. + * + * @return The Component + */ + @Suppress("LongParameterList") + fun get( + savedStateRegistryOwner: SavedStateRegistryOwner, + viewModelStoreOwner: ViewModelStoreOwner, + lifecycleOwner: LifecycleOwner, + checkoutSession: CheckoutSession, + storedPaymentMethod: StoredPaymentMethod, + checkoutConfiguration: CheckoutConfiguration, + application: Application, + componentCallback: ComponentCallbackT, + key: String?, + ): ComponentT + + //endregion + + //region Payment method specific configuration + /** * Get a [PaymentComponent] with a stored payment method and a checkout session. You only need to integrate with * the /sessions endpoint to create a session and the component will automatically handle the rest of the payment @@ -139,4 +323,6 @@ interface SessionStoredPaymentComponentProvider< componentCallback: ComponentCallbackT, key: String?, ): ComponentT + + //endregion } diff --git a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt index 0bd517168e..2e7092f459 100644 --- a/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt +++ b/sessions-core/src/main/java/com/adyen/checkout/sessions/core/internal/ui/model/SessionParamsFactory.kt @@ -9,36 +9,33 @@ package com.adyen.checkout.sessions.core.internal.ui.model import androidx.annotation.RestrictTo -import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentConfiguration import com.adyen.checkout.components.core.internal.ui.model.SessionInstallmentOptionsParams import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.LocaleUtil +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.sessions.core.CheckoutSession -import com.adyen.checkout.sessions.core.SessionSetupConfiguration import com.adyen.checkout.sessions.core.internal.data.model.SessionDetails +import com.adyen.checkout.sessions.core.internal.data.model.mapToDetails +import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) object SessionParamsFactory { // Used for components fun create(checkoutSession: CheckoutSession): SessionParams { - return create( - checkoutSession.sessionSetupResponse.configuration, - checkoutSession.sessionSetupResponse.amount, - checkoutSession.sessionSetupResponse.returnUrl - ) + return checkoutSession.mapToDetails().mapToParams() } // Used for Drop-in fun create(sessionDetails: SessionDetails): SessionParams { - return create(sessionDetails.sessionSetupConfiguration, sessionDetails.amount, sessionDetails.returnUrl) + return sessionDetails.mapToParams() } - private fun create( - sessionSetupConfiguration: SessionSetupConfiguration?, - amount: Amount?, - returnUrl: String?, - ): SessionParams { + private fun SessionDetails.mapToParams(): SessionParams { return SessionParams( + environment = environment, + clientKey = clientKey, enableStoreDetails = sessionSetupConfiguration?.enableStoreDetails, installmentConfiguration = SessionInstallmentConfiguration( installmentOptions = sessionSetupConfiguration?.installmentOptions?.map { @@ -48,10 +45,23 @@ object SessionParamsFactory { values = it.value?.values, ) }?.toMap(), - showInstallmentAmount = sessionSetupConfiguration?.showInstallmentAmount + showInstallmentAmount = sessionSetupConfiguration?.showInstallmentAmount, ), + showRemovePaymentMethodButton = sessionSetupConfiguration?.showRemovePaymentMethodButton, amount = amount, returnUrl = returnUrl, + shopperLocale = getShopperLocale(shopperLocale), ) } + + private fun getShopperLocale(shopperLocaleString: String?): Locale? { + if (shopperLocaleString == null) return null + return runCatching { + LocaleUtil.fromLanguageTag(shopperLocaleString) + }.getOrElse { + // if we cannot parse the locale coming from the API we should not fail the payment + adyenLog(AdyenLogLevel.ERROR) { "Failed to parse sessions locale $shopperLocaleString" } + null + } + } } diff --git a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/SessionComponentCallbackTest.kt b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/SessionComponentCallbackTest.kt new file mode 100644 index 0000000000..ccd48c372c --- /dev/null +++ b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/SessionComponentCallbackTest.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 2/2/2024. + */ + +package com.adyen.checkout.sessions.core + +import com.adyen.checkout.core.PermissionHandlerCallback +import org.junit.jupiter.api.Test +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +internal class SessionComponentCallbackTest { + + private val componentCallback = TestSessionComponentCallback() + + @Test + fun `when onPermissionRequest is called, then onPermissionRequestNotHandled is invoked`() { + val requiredPermission = "permission" + val permissionCallback = mock() + + componentCallback.onPermissionRequest(requiredPermission, permissionCallback) + + verify(permissionCallback).onPermissionRequestNotHandled(eq(requiredPermission)) + } +} diff --git a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/TestComponentState.kt b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestComponentState.kt similarity index 85% rename from sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/TestComponentState.kt rename to sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestComponentState.kt index cb45fc2063..62f6539f3c 100644 --- a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/TestComponentState.kt +++ b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestComponentState.kt @@ -3,10 +3,10 @@ * * This file is open source and available under the MIT license. See the LICENSE file for more info. * - * Created by ozgur on 27/2/2023. + * Created by ararat on 2/2/2024. */ -package com.adyen.checkout.sessions.core.internal +package com.adyen.checkout.sessions.core import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.PaymentComponentState diff --git a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/TestPaymentMethod.kt b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestPaymentMethod.kt similarity index 91% rename from sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/TestPaymentMethod.kt rename to sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestPaymentMethod.kt index 95abfb0721..f5443e8822 100644 --- a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/TestPaymentMethod.kt +++ b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestPaymentMethod.kt @@ -6,7 +6,7 @@ * Created by josephj on 2/2/2023. */ -package com.adyen.checkout.sessions.core.internal +package com.adyen.checkout.sessions.core import android.os.Parcel import com.adyen.checkout.components.core.paymentmethod.PaymentMethodDetails diff --git a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestSessionComponentCallback.kt b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestSessionComponentCallback.kt new file mode 100644 index 0000000000..e520435efb --- /dev/null +++ b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/TestSessionComponentCallback.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 2/2/2024. + */ + +package com.adyen.checkout.sessions.core + +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.action.Action + +internal class TestSessionComponentCallback : SessionComponentCallback { + override fun onAction(action: Action) { + // Not necessary + } + + override fun onFinished(result: SessionPaymentResult) { + // Not necessary + } + + override fun onError(componentError: ComponentError) { + // Not necessary + } +} diff --git a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandlerTest.kt b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandlerTest.kt index e302ee9a1f..97cfdf6f3d 100644 --- a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandlerTest.kt +++ b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionComponentEventHandlerTest.kt @@ -16,12 +16,13 @@ import com.adyen.checkout.components.core.PaymentComponentState import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.internal.BaseComponentCallback import com.adyen.checkout.components.core.internal.PaymentComponentEvent -import com.adyen.checkout.core.AdyenLogger +import com.adyen.checkout.core.PermissionHandlerCallback import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.SessionModel import com.adyen.checkout.sessions.core.SessionPaymentResult +import com.adyen.checkout.sessions.core.TestComponentState +import com.adyen.checkout.test.LoggingExtension import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -45,7 +46,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class) +@ExtendWith(MockitoExtension::class, LoggingExtension::class) internal class SessionComponentEventHandlerTest( @Mock private val sessionInteractor: SessionInteractor, @Mock private val sessionSavedStateHandleContainer: SessionSavedStateHandleContainer, @@ -56,7 +57,6 @@ internal class SessionComponentEventHandlerTest( @BeforeEach fun beforeEach() { sessionComponentEventHandler = SessionComponentEventHandler(sessionInteractor, sessionSavedStateHandleContainer) - AdyenLogger.setLogLevel(Logger.NONE) } @Test @@ -85,7 +85,7 @@ internal class SessionComponentEventHandlerTest( assertThrows { sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - object : BaseComponentCallback {} + object : BaseComponentCallback {}, ) } } @@ -100,7 +100,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) verify(callback).onLoading(true) @@ -116,7 +116,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) verify(callback).onAction(action) @@ -131,7 +131,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) val errorCaptor = argumentCaptor() @@ -148,7 +148,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) verify(callback).onFinished(result) @@ -163,7 +163,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) verify(callback).onFinished(result) @@ -178,7 +178,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) verify(callback).onFinished(result) @@ -192,7 +192,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Submit(createPaymentComponentState()), - callback + callback, ) verify(sessionSavedStateHandleContainer).isFlowTakenOver = true @@ -209,7 +209,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.ActionDetails(ActionComponentData()), - callback + callback, ) verify(callback).onLoading(true) @@ -225,7 +225,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.ActionDetails(ActionComponentData()), - callback + callback, ) verify(callback).onAction(action) @@ -240,7 +240,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.ActionDetails(ActionComponentData()), - callback + callback, ) val errorCaptor = argumentCaptor() @@ -257,7 +257,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.ActionDetails(ActionComponentData()), - callback + callback, ) verify(callback).onFinished(result) @@ -271,7 +271,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.ActionDetails(ActionComponentData()), - callback + callback, ) verify(sessionSavedStateHandleContainer).isFlowTakenOver = true @@ -289,7 +289,7 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.StateChanged(componentState), - callback + callback, ) verify(callback).onStateChanged(componentState) @@ -307,12 +307,31 @@ internal class SessionComponentEventHandlerTest( sessionComponentEventHandler.onPaymentComponentEvent( PaymentComponentEvent.Error(error), - callback + callback, ) verify(callback).onError(error) } } + + @Nested + @DisplayName("is PermissionRequested") + inner class PermissionRequestedTest { + + @Test + fun `then permission requested should be propagated`() = runTest { + val callback = mock>>() + val requiredPermission = "Required Permission" + val permissionCallback = mock() + + sessionComponentEventHandler.onPaymentComponentEvent( + PaymentComponentEvent.PermissionRequest(requiredPermission, permissionCallback), + callback, + ) + + verify(callback).onPermissionRequest(requiredPermission, permissionCallback) + } + } } private fun createPaymentComponentState() = TestComponentState( diff --git a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionInteractorTest.kt b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionInteractorTest.kt index 4606c06da8..9cc6683837 100644 --- a/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionInteractorTest.kt +++ b/sessions-core/src/test/java/com/adyen/checkout/sessions/core/internal/SessionInteractorTest.kt @@ -19,19 +19,21 @@ import com.adyen.checkout.components.core.PaymentMethodsApiResponse import com.adyen.checkout.components.core.action.Action import com.adyen.checkout.components.core.action.RedirectAction import com.adyen.checkout.components.core.internal.util.StatusResponseUtils -import com.adyen.checkout.core.AdyenLogger import com.adyen.checkout.core.exception.CheckoutException -import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.sessions.core.SessionModel import com.adyen.checkout.sessions.core.SessionPaymentResult import com.adyen.checkout.sessions.core.SessionSetupConfiguration import com.adyen.checkout.sessions.core.SessionSetupResponse +import com.adyen.checkout.sessions.core.TestComponentState +import com.adyen.checkout.sessions.core.TestPaymentMethod import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository import com.adyen.checkout.sessions.core.internal.data.model.SessionBalanceResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionCancelOrderResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionDetailsResponse +import com.adyen.checkout.sessions.core.internal.data.model.SessionDisableTokenResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionOrderResponse import com.adyen.checkout.sessions.core.internal.data.model.SessionPaymentsResponse +import com.adyen.checkout.test.LoggingExtension import com.adyen.checkout.test.TestDispatcherExtension import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -50,7 +52,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class) +@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class, LoggingExtension::class) internal class SessionInteractorTest( @Mock private val sessionRepository: SessionRepository, ) { @@ -60,7 +62,6 @@ internal class SessionInteractorTest( @BeforeEach fun before() { sessionInteractor = createSessionInteractor() - AdyenLogger.setLogLevel(Logger.NONE) } @Nested @@ -87,7 +88,7 @@ internal class SessionInteractorTest( sessionData = mockResponse.sessionData, resultCode = mockResponse.resultCode, order = mockResponse.order, - ) + ), ) assertEquals(expectedResult, result) @@ -101,7 +102,7 @@ internal class SessionInteractorTest( fun `action is required then Action is returned and session data is updated`() = runTest { sessionInteractor.sessionFlow.test { val mockResponse = createSessionPaymentsResponse( - action = RedirectAction() + action = RedirectAction(), ) whenever(sessionRepository.submitPayment(any(), any())) doReturn Result.success(mockResponse) @@ -109,7 +110,7 @@ internal class SessionInteractorTest( val result = sessionInteractor.onPaymentsCallRequested(TEST_COMPONENT_STATE, { false }, "") val expectedResult = SessionCallResult.Payments.Action( - requireNotNull(mockResponse.action) + requireNotNull(mockResponse.action), ) assertEquals(expectedResult, result) @@ -124,7 +125,7 @@ internal class SessionInteractorTest( runTest { sessionInteractor.sessionFlow.test { val mockResponse = createSessionPaymentsResponse( - order = TEST_ORDER_RESPONSE + order = TEST_ORDER_RESPONSE, ) whenever(sessionRepository.submitPayment(any(), any())) doReturn Result.success(mockResponse) @@ -139,7 +140,7 @@ internal class SessionInteractorTest( sessionData = mockResponse.sessionData, resultCode = mockResponse.resultCode, order = mockResponse.order, - ) + ), ) assertEquals(expectedResult, result) @@ -151,7 +152,7 @@ internal class SessionInteractorTest( fun `a partial payment is fully paid then Finished is returned and session data is updated`() = runTest { sessionInteractor.sessionFlow.test { val mockResponse = createSessionPaymentsResponse( - order = TEST_ORDER_RESPONSE.copy(remainingAmount = Amount("USD", 0)) + order = TEST_ORDER_RESPONSE.copy(remainingAmount = Amount("USD", 0)), ) whenever(sessionRepository.submitPayment(any(), any())) doReturn Result.success(mockResponse) @@ -165,7 +166,7 @@ internal class SessionInteractorTest( sessionData = mockResponse.sessionData, resultCode = mockResponse.resultCode, order = mockResponse.order, - ) + ), ) assertEquals(expectedResult, result) @@ -195,7 +196,7 @@ internal class SessionInteractorTest( sessionData = mockResponse.sessionData, resultCode = mockResponse.resultCode, order = mockResponse.order, - ) + ), ) assertEquals(expectedResult, result) @@ -249,7 +250,7 @@ internal class SessionInteractorTest( @Test fun `merchant doesn't handle call then an exception is thrown and isFlowTakenOver stays true`() = runTest { assertThrows( - "Sessions flow was already taken over in a previous call, makePayment should be implemented" + "Sessions flow was already taken over in a previous call, makePayment should be implemented", ) { sessionInteractor.onPaymentsCallRequested(TEST_COMPONENT_STATE, { false }, "makePayment") } @@ -283,7 +284,7 @@ internal class SessionInteractorTest( sessionData = mockResponse.sessionData, resultCode = mockResponse.resultCode, order = mockResponse.order, - ) + ), ) assertEquals(expectedResult, result) @@ -297,7 +298,7 @@ internal class SessionInteractorTest( fun `action is required then Action is returned and session data is updated`() = runTest { sessionInteractor.sessionFlow.test { val mockResponse = createSessionDetailsResponse( - action = RedirectAction() + action = RedirectAction(), ) whenever(sessionRepository.submitDetails(any(), any())) doReturn Result.success(mockResponse) @@ -305,7 +306,7 @@ internal class SessionInteractorTest( val result = sessionInteractor.onDetailsCallRequested(ActionComponentData(), { false }, "") val expectedResult = SessionCallResult.Details.Action( - requireNotNull(mockResponse.action) + requireNotNull(mockResponse.action), ) assertEquals(expectedResult, result) @@ -359,7 +360,7 @@ internal class SessionInteractorTest( @Test fun `merchant doesn't handle call then an exception is thrown and isFlowTakenOver stays true`() = runTest { assertThrows( - "Sessions flow was already taken over in a previous call, makeDetails should be implemented" + "Sessions flow was already taken over in a previous call, makeDetails should be implemented", ) { sessionInteractor.onDetailsCallRequested(ActionComponentData(), { false }, "makeDetails") } @@ -394,7 +395,7 @@ internal class SessionInteractorTest( BalanceResult( balance = mockResponse.balance, transactionLimit = mockResponse.transactionLimit, - ) + ), ) assertEquals(expectedResult, result) @@ -404,32 +405,6 @@ internal class SessionInteractorTest( } } - @Test - fun `balance is zero then Error is returned and session data is updated`() = runTest { - sessionInteractor.sessionFlow.test { - val mockResponse = SessionBalanceResponse( - sessionData = "session_data_updated", - balance = Amount("USD", 0), - transactionLimit = null, - ) - - whenever(sessionRepository.checkBalance(any(), any())) doReturn Result.success(mockResponse) - - val result = sessionInteractor.checkBalance(TEST_COMPONENT_STATE, { false }, "") - - assertTrue(result is SessionCallResult.Balance.Error) - require(result is SessionCallResult.Balance.Error) - - assertTrue(result.throwable is CheckoutException) - require(result.throwable is CheckoutException) - - assertEquals("Not enough balance", result.throwable.message) - - val expectedSessionModel = TEST_SESSION_MODEL.copy(sessionData = mockResponse.sessionData) - assertEquals(expectedSessionModel, expectMostRecentItem()) - } - } - @Test fun `an error is thrown then Error is returned`() = runTest { val exception = Exception("failed for testing") @@ -474,7 +449,7 @@ internal class SessionInteractorTest( @Test fun `merchant doesn't handle call then an exception is thrown and isFlowTakenOver stays true`() = runTest { assertThrows( - "Sessions flow was already taken over in a previous call, makeBalance should be implemented" + "Sessions flow was already taken over in a previous call, makeBalance should be implemented", ) { sessionInteractor.checkBalance(TEST_COMPONENT_STATE, { false }, "makeBalance") } @@ -511,7 +486,7 @@ internal class SessionInteractorTest( orderData = mockResponse.orderData, amount = null, remainingAmount = null, - ) + ), ) assertEquals(expectedResult, result) @@ -565,7 +540,7 @@ internal class SessionInteractorTest( @Test fun `merchant doesn't handle call then an exception is thrown and isFlowTakenOver stays true`() = runTest { assertThrows( - "Sessions flow was already taken over in a previous call, createOrder should be implemented" + "Sessions flow was already taken over in a previous call, createOrder should be implemented", ) { sessionInteractor.createOrder({ false }, "createOrder") } @@ -647,7 +622,7 @@ internal class SessionInteractorTest( @Test fun `merchant doesn't handle call then an exception is thrown and isFlowTakenOver stays true`() = runTest { assertThrows( - "Sessions flow was already taken over in a previous call, cancelOrder should be implemented" + "Sessions flow was already taken over in a previous call, cancelOrder should be implemented", ) { sessionInteractor.cancelOrder(TEST_ORDER_REQUEST, { false }, "cancelOrder") } @@ -707,6 +682,39 @@ internal class SessionInteractorTest( } } + @Nested + @DisplayName("when disable token call is requested and") + inner class RemoveStoredPaymentMethodCallTest { + + @Test + fun `it is successful then session data is updated`() = runTest { + sessionInteractor.sessionFlow.test { + val mockResponse = SessionDisableTokenResponse(sessionData = "session_data_updated") + whenever(sessionRepository.disableToken(any(), any())) doReturn Result.success(mockResponse) + + val result = sessionInteractor.removeStoredPaymentMethod("stored_payment_method_id") + + val expectedResult = SessionCallResult.RemoveStoredPaymentMethod.Successful + assertEquals(expectedResult, result) + + val expectedSessionModel = TEST_SESSION_MODEL.copy(sessionData = mockResponse.sessionData) + assertEquals(expectedSessionModel, expectMostRecentItem()) + } + } + + @Test + fun `an error is thrown then Error is returned`() = runTest { + val exception = Exception("failed for testing") + + whenever(sessionRepository.disableToken(any(), any())) doReturn Result.failure(exception) + + val result = sessionInteractor.removeStoredPaymentMethod("stored_payment_method_id") + + val expectedResult = SessionCallResult.RemoveStoredPaymentMethod.Error(exception) + assertEquals(expectedResult, result) + } + } + private fun createSessionInteractor( sessionModel: SessionModel = TEST_SESSION_MODEL, isFlowTakenOver: Boolean = false @@ -756,7 +764,8 @@ internal class SessionInteractorTest( expiresAt: String = "", returnUrl: String = "", paymentMethods: PaymentMethodsApiResponse? = PaymentMethodsApiResponse(), - configuration: SessionSetupConfiguration? = null + configuration: SessionSetupConfiguration? = null, + shopperLocale: String? = null, ): SessionSetupResponse { return SessionSetupResponse( id = id, @@ -765,7 +774,8 @@ internal class SessionInteractorTest( expiresAt = expiresAt, paymentMethodsApiResponse = paymentMethods, returnUrl = returnUrl, - configuration = configuration + configuration = configuration, + shopperLocale = shopperLocale, ) } @@ -774,7 +784,7 @@ internal class SessionInteractorTest( private val TEST_SESSION_MODEL = SessionModel( id = "session_id", - sessionData = "session_data_initial" + sessionData = "session_data_initial", ) private val TEST_ORDER_REQUEST = OrderRequest( diff --git a/seven-eleven/build.gradle b/seven-eleven/build.gradle index ea4ed13715..2ad81046b2 100644 --- a/seven-eleven/build.gradle +++ b/seven-eleven/build.gradle @@ -32,4 +32,7 @@ android { dependencies { api project(':econtext') + + testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito } diff --git a/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/SevenElevenConfiguration.kt b/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/SevenElevenConfiguration.kt index 1eead288db..8a29c9112b 100644 --- a/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/SevenElevenConfiguration.kt +++ b/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/SevenElevenConfiguration.kt @@ -12,6 +12,9 @@ import android.content.Context import com.adyen.checkout.action.core.GenericActionConfiguration import com.adyen.checkout.components.core.Amount import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.internal.util.CheckoutConfigurationMarker import com.adyen.checkout.core.Environment import com.adyen.checkout.econtext.internal.EContextConfiguration import kotlinx.parcelize.Parcelize @@ -23,7 +26,7 @@ import java.util.Locale @Suppress("LongParameterList") @Parcelize class SevenElevenConfiguration private constructor( - override val shopperLocale: Locale, + override val shopperLocale: Locale?, override val environment: Environment, override val clientKey: String, override val analyticsConfiguration: AnalyticsConfiguration?, @@ -37,6 +40,22 @@ class SevenElevenConfiguration private constructor( */ class Builder : EContextConfiguration.Builder { + /** + * Initialize a configuration builder with the required fields. + * + * The shopper locale will match the value passed to the API with the sessions flow, or the primary user locale + * on the device otherwise. Check out the + * [Sessions API documentation](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) on how to set + * this value. + * + * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. + * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. + */ + constructor(environment: Environment, clientKey: String) : super( + environment, + clientKey, + ) + /** * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale. * @@ -44,14 +63,15 @@ class SevenElevenConfiguration private constructor( * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. * @param clientKey Your Client Key used for internal network calls from the SDK to Adyen. */ + @Deprecated("You can omit the context parameter") constructor(context: Context, environment: Environment, clientKey: String) : super( context, environment, - clientKey + clientKey, ) /** - * Initialize a configuration builder with the required fields. + * Initialize a configuration builder with the required fields and a shopper locale. * * @param shopperLocale The [Locale] of the shopper. * @param environment The [Environment] to be used for internal network calls from the SDK to Adyen. @@ -76,3 +96,38 @@ class SevenElevenConfiguration private constructor( } } } + +fun CheckoutConfiguration.sevenEleven( + configuration: @CheckoutConfigurationMarker SevenElevenConfiguration.Builder.() -> Unit = {} +): CheckoutConfiguration { + val config = SevenElevenConfiguration.Builder(environment, clientKey) + .apply { + shopperLocale?.let { setShopperLocale(it) } + amount?.let { setAmount(it) } + analyticsConfiguration?.let { setAnalyticsConfiguration(it) } + } + .apply(configuration) + .build() + addConfiguration(PaymentMethodTypes.ECONTEXT_SEVEN_ELEVEN, config) + return this +} + +fun CheckoutConfiguration.getSevenElevenConfiguration(): SevenElevenConfiguration? { + return getConfiguration(PaymentMethodTypes.ECONTEXT_SEVEN_ELEVEN) +} + +internal fun SevenElevenConfiguration.toCheckoutConfiguration(): CheckoutConfiguration { + return CheckoutConfiguration( + shopperLocale = shopperLocale, + environment = environment, + clientKey = clientKey, + amount = amount, + analyticsConfiguration = analyticsConfiguration, + ) { + addConfiguration(PaymentMethodTypes.ECONTEXT_SEVEN_ELEVEN, this@toCheckoutConfiguration) + + genericActionConfiguration.getAllConfigurations().forEach { + addActionConfiguration(it) + } + } +} diff --git a/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/internal/provider/SevenElevenComponentProvider.kt b/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/internal/provider/SevenElevenComponentProvider.kt index 00c8235e84..871e3316c6 100644 --- a/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/internal/provider/SevenElevenComponentProvider.kt +++ b/seven-eleven/src/main/java/com/adyen/checkout/seveneleven/internal/provider/SevenElevenComponentProvider.kt @@ -11,33 +11,33 @@ package com.adyen.checkout.seveneleven.internal.provider import androidx.annotation.RestrictTo import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate +import com.adyen.checkout.components.core.CheckoutConfiguration import com.adyen.checkout.components.core.PaymentComponentData import com.adyen.checkout.components.core.internal.ComponentEventHandler import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository -import com.adyen.checkout.components.core.internal.ui.model.ComponentParams -import com.adyen.checkout.components.core.internal.ui.model.SessionParams +import com.adyen.checkout.components.core.internal.ui.model.DropInOverrideParams import com.adyen.checkout.components.core.paymentmethod.SevenElevenPaymentMethod import com.adyen.checkout.econtext.internal.provider.EContextComponentProvider import com.adyen.checkout.econtext.internal.ui.EContextDelegate import com.adyen.checkout.seveneleven.SevenElevenComponent import com.adyen.checkout.seveneleven.SevenElevenComponentState import com.adyen.checkout.seveneleven.SevenElevenConfiguration +import com.adyen.checkout.seveneleven.getSevenElevenConfiguration +import com.adyen.checkout.seveneleven.toCheckoutConfiguration class SevenElevenComponentProvider @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor( - overrideComponentParams: ComponentParams? = null, - overrideSessionParams: SessionParams? = null, + dropInOverrideParams: DropInOverrideParams? = null, analyticsRepository: AnalyticsRepository? = null, ) : EContextComponentProvider< SevenElevenComponent, SevenElevenConfiguration, SevenElevenPaymentMethod, - SevenElevenComponentState + SevenElevenComponentState, >( componentClass = SevenElevenComponent::class.java, - overrideComponentParams = overrideComponentParams, - overrideSessionParams = overrideSessionParams, + dropInOverrideParams = dropInOverrideParams, analyticsRepository = analyticsRepository, ) { @@ -65,6 +65,14 @@ constructor( return SevenElevenPaymentMethod() } + override fun getConfiguration(checkoutConfiguration: CheckoutConfiguration): SevenElevenConfiguration? { + return checkoutConfiguration.getSevenElevenConfiguration() + } + + override fun getCheckoutConfiguration(configuration: SevenElevenConfiguration): CheckoutConfiguration { + return configuration.toCheckoutConfiguration() + } + override fun getSupportedPaymentMethods(): List { return SevenElevenComponent.PAYMENT_METHOD_TYPES } diff --git a/seven-eleven/src/test/java/com/adyen/checkout/seveneleven/SevenElevenConfigurationTest.kt b/seven-eleven/src/test/java/com/adyen/checkout/seveneleven/SevenElevenConfigurationTest.kt new file mode 100644 index 0000000000..bb87430b7a --- /dev/null +++ b/seven-eleven/src/test/java/com/adyen/checkout/seveneleven/SevenElevenConfigurationTest.kt @@ -0,0 +1,88 @@ +package com.adyen.checkout.seveneleven + +import com.adyen.checkout.components.core.Amount +import com.adyen.checkout.components.core.AnalyticsConfiguration +import com.adyen.checkout.components.core.AnalyticsLevel +import com.adyen.checkout.components.core.CheckoutConfiguration +import com.adyen.checkout.core.Environment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.util.Locale + +class SevenElevenConfigurationTest { + + @Test + fun `when creating the configuration through CheckoutConfiguration, then it should be the same as when the builder is used`() { + val checkoutConfiguration = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) { + sevenEleven { + setSubmitButtonVisible(false) + } + } + + val actual = checkoutConfiguration.getSevenElevenConfiguration() + + val expected = SevenElevenConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + assertEquals(expected.shopperLocale, actual?.shopperLocale) + assertEquals(expected.environment, actual?.environment) + assertEquals(expected.clientKey, actual?.clientKey) + assertEquals(expected.amount, actual?.amount) + assertEquals(expected.analyticsConfiguration, actual?.analyticsConfiguration) + assertEquals(expected.isSubmitButtonVisible, actual?.isSubmitButtonVisible) + } + + @Test + fun `when the configuration is mapped to CheckoutConfiguration, then CheckoutConfiguration is created correctly`() { + val config = SevenElevenConfiguration.Builder( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + ) + .setAmount(Amount("EUR", 123L)) + .setAnalyticsConfiguration(AnalyticsConfiguration(AnalyticsLevel.ALL)) + .setSubmitButtonVisible(false) + .build() + + val actual = config.toCheckoutConfiguration() + + val expected = CheckoutConfiguration( + shopperLocale = Locale.US, + environment = Environment.TEST, + clientKey = TEST_CLIENT_KEY, + amount = Amount("EUR", 123L), + analyticsConfiguration = AnalyticsConfiguration(AnalyticsLevel.ALL), + ) + + assertEquals(expected.shopperLocale, actual.shopperLocale) + assertEquals(expected.environment, actual.environment) + assertEquals(expected.clientKey, actual.clientKey) + assertEquals(expected.amount, actual.amount) + assertEquals(expected.analyticsConfiguration, actual.analyticsConfiguration) + + val actualSevenElevenConfig = actual.getSevenElevenConfiguration() + assertEquals(config.shopperLocale, actualSevenElevenConfig?.shopperLocale) + assertEquals(config.environment, actualSevenElevenConfig?.environment) + assertEquals(config.clientKey, actualSevenElevenConfig?.clientKey) + assertEquals(config.amount, actualSevenElevenConfig?.amount) + assertEquals(config.analyticsConfiguration, actualSevenElevenConfig?.analyticsConfiguration) + assertEquals(config.isSubmitButtonVisible, actualSevenElevenConfig?.isSubmitButtonVisible) + } + + companion object { + private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty" + } +} diff --git a/test-core/build.gradle b/test-core/build.gradle index fa529ef335..449f5dcf46 100644 --- a/test-core/build.gradle +++ b/test-core/build.gradle @@ -29,6 +29,7 @@ android { } dependencies { + implementation project(':checkout-core') implementation libraries.androidx.lifecycle implementation testLibraries.junit5 diff --git a/test-core/src/main/java/com/adyen/checkout/test/LoggingExtension.kt b/test-core/src/main/java/com/adyen/checkout/test/LoggingExtension.kt new file mode 100644 index 0000000000..bcc2409332 --- /dev/null +++ b/test-core/src/main/java/com/adyen/checkout/test/LoggingExtension.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 5/2/2024. + */ + +package com.adyen.checkout.test + +import androidx.annotation.RestrictTo +import com.adyen.checkout.core.AdyenLogger +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext + +@RestrictTo(RestrictTo.Scope.TESTS, RestrictTo.Scope.LIBRARY_GROUP) +class LoggingExtension : BeforeAllCallback, AfterAllCallback { + + override fun beforeAll(context: ExtensionContext?) { + AdyenLogger.setLogger(PrintLogger()) + } + + override fun afterAll(context: ExtensionContext?) { + AdyenLogger.resetLogger() + } +} diff --git a/test-core/src/main/java/com/adyen/checkout/test/PrintLogger.kt b/test-core/src/main/java/com/adyen/checkout/test/PrintLogger.kt new file mode 100644 index 0000000000..44b822fb91 --- /dev/null +++ b/test-core/src/main/java/com/adyen/checkout/test/PrintLogger.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by oscars on 5/2/2024. + */ + +package com.adyen.checkout.test + +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.AdyenLogger +import java.io.PrintWriter +import java.io.StringWriter + +internal class PrintLogger : AdyenLogger { + + override fun shouldLog(level: AdyenLogLevel): Boolean = true + + override fun setLogLevel(level: AdyenLogLevel) = Unit + + override fun log(level: AdyenLogLevel, tag: String, message: String, throwable: Throwable?) { + println("${getLogColor(level)}${concatThrowable(message, throwable)}$RESET_COLOR") + } + + private fun getLogColor(level: AdyenLogLevel): String { + return when (level) { + AdyenLogLevel.WARN -> YELLOW_COLOR + AdyenLogLevel.ERROR, + AdyenLogLevel.ASSERT -> RED_COLOR + + else -> "" + } + } + + private fun concatThrowable(message: String, throwable: Throwable?): String { + return if (throwable != null) { + val stringWriter = StringWriter(STRING_WRITER_SIZE) + val printWriter = PrintWriter(stringWriter, false) + throwable.printStackTrace(printWriter) + printWriter.flush() + "$message: $stringWriter" + } else { + message + } + } + + companion object { + private const val YELLOW_COLOR = "\u001B[33m" + private const val RED_COLOR = "\u001B[31m" + private const val RESET_COLOR = "\u001B[0m" + + private const val STRING_WRITER_SIZE = 16 + } +} diff --git a/test-core/src/main/java/com/adyen/checkout/test/TestDispatcherExtension.kt b/test-core/src/main/java/com/adyen/checkout/test/TestDispatcherExtension.kt index 26ceb51134..d1a5a79bee 100644 --- a/test-core/src/main/java/com/adyen/checkout/test/TestDispatcherExtension.kt +++ b/test-core/src/main/java/com/adyen/checkout/test/TestDispatcherExtension.kt @@ -9,65 +9,29 @@ package com.adyen.checkout.test import androidx.annotation.RestrictTo -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.extension.ParameterContext -import org.junit.jupiter.api.extension.ParameterResolutionException -import org.junit.jupiter.api.extension.ParameterResolver /** * JUnit 5 extension that replaces [Dispatchers.Main] with a test dispatcher. This gives control over how the dispatcher * executes it's work. - * - * Example: - * ``` - * @ExtendWith(TestDispatcherExtension::class) - * internal class ExampleTest { - * - * @Test - * fun test(dispatcher: CoroutineDispatcher) = runTest(dispatcher) { - * ... - * } - * } - * ``` */ @OptIn(ExperimentalCoroutinesApi::class) @RestrictTo(RestrictTo.Scope.TESTS) -class TestDispatcherExtension : BeforeEachCallback, AfterEachCallback, ParameterResolver { +class TestDispatcherExtension : BeforeEachCallback, AfterEachCallback { override fun beforeEach(context: ExtensionContext) { val testDispatcher = UnconfinedTestDispatcher() Dispatchers.setMain(testDispatcher) - context.getStore(NAMESPACE).put(DISPATCHER, testDispatcher) } override fun afterEach(context: ExtensionContext) { Dispatchers.resetMain() - context.getStore(NAMESPACE).remove(DISPATCHER, TestDispatcher::class.java) - } - - @Throws(ParameterResolutionException::class) - @Suppress("NewApi") - override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean = - // It's safe to ignore the warning on getType(), because this code is used in JVM unit tests and not - // android related. - parameterContext.parameter.type.isAssignableFrom(CoroutineDispatcher::class.java) || - parameterContext.parameter.type.isAssignableFrom(TestDispatcher::class.java) - - @Throws(ParameterResolutionException::class) - override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any = - extensionContext.getStore(NAMESPACE).get(DISPATCHER) - - companion object { - private val NAMESPACE = ExtensionContext.Namespace.create("com.adyen.checkout") - private const val DISPATCHER = "dispatcher" } } diff --git a/ui-core/build.gradle b/ui-core/build.gradle index 01ef8cbf74..6d3a94fe7f 100644 --- a/ui-core/build.gradle +++ b/ui-core/build.gradle @@ -52,6 +52,8 @@ dependencies { //Tests testImplementation testLibraries.json testImplementation testLibraries.junit5 + testImplementation testLibraries.mockito testImplementation testLibraries.kotlinCoroutines testImplementation testLibraries.robolectric + testImplementation testLibraries.mockito } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt index 0ddb4dd73e..9f311f21e0 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/AdyenComponentView.kt @@ -20,8 +20,8 @@ import com.adyen.checkout.components.core.internal.Component import com.adyen.checkout.components.core.internal.ui.ComponentDelegate import com.adyen.checkout.components.core.internal.ui.model.ComponentParams import com.adyen.checkout.components.core.internal.util.createLocalizedContext -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.databinding.AdyenComponentViewBinding import com.adyen.checkout.ui.core.internal.ui.AmountButtonComponentViewType import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType @@ -91,13 +91,13 @@ class AdyenComponentView @JvmOverloads constructor( binding.frameLayoutComponentContainer.removeAllViews() if (componentViewType == null) { - Logger.i(TAG, "Component view type is null, ignoring.") + adyenLog(AdyenLogLevel.INFO) { "Component view type is null, ignoring." } return@onEach } val delegate = component.delegate if (delegate !is ViewProvidingDelegate) { - Logger.i(TAG, "View attached to non viewable component, ignoring.") + adyenLog(AdyenLogLevel.INFO) { "View attached to non viewable component, ignoring." } return@onEach } @@ -201,8 +201,4 @@ class AdyenComponentView @JvmOverloads constructor( if (isInteractionBlocked) return true return super.onInterceptTouchEvent(ev) } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt index f24ae637c5..d225ad10fa 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt @@ -16,10 +16,10 @@ import androidx.annotation.RequiresApi import androidx.annotation.RestrictTo import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.util.ThemeUtil import org.json.JSONException import org.json.JSONObject @@ -30,7 +30,7 @@ class DefaultRedirectHandler : RedirectHandler { private var onRedirectListener: (() -> Unit)? = null override fun parseRedirectResult(data: Uri?): JSONObject { - Logger.d(TAG, "parseRedirectResult - $data") + adyenLog(AdyenLogLevel.DEBUG) { "parseRedirectResult - $data" } data ?: throw CheckoutException("Received a null redirect Uri") @@ -76,7 +76,7 @@ class DefaultRedirectHandler : RedirectHandler { return } - Logger.e(TAG, "Could not launch url") + adyenLog(AdyenLogLevel.ERROR) { "Could not launch url" } throw ComponentException("Launching redirect failed.") } @@ -113,7 +113,7 @@ class DefaultRedirectHandler : RedirectHandler { // If the list is empty, no native app handlers were found. if (resolvedSpecializedSet.isEmpty()) { - Logger.d(TAG, "launchNativeBeforeApi30 - could not find native app to redirect with") + adyenLog(AdyenLogLevel.DEBUG) { "launchNativeBeforeApi30 - could not find native app to redirect with" } return false } @@ -123,10 +123,10 @@ class DefaultRedirectHandler : RedirectHandler { @Suppress("SwallowedException") return try { context.startActivity(specializedActivityIntent) - Logger.d(TAG, "launchNativeBeforeApi30 - redirect successful with native app") + adyenLog(AdyenLogLevel.DEBUG) { "launchNativeBeforeApi30 - redirect successful with native app" } true } catch (e: ActivityNotFoundException) { - Logger.d(TAG, "launchNativeBeforeApi30 - could not find native app to redirect with") + adyenLog(AdyenLogLevel.DEBUG) { "launchNativeBeforeApi30 - could not find native app to redirect with" } false } } @@ -137,15 +137,15 @@ class DefaultRedirectHandler : RedirectHandler { .addCategory(Intent.CATEGORY_BROWSABLE) .addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or - Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER + Intent.FLAG_ACTIVITY_REQUIRE_NON_BROWSER, ) @Suppress("SwallowedException") return try { context.startActivity(nativeAppIntent) - Logger.d(TAG, "launchNativeApi30 - redirect successful with native app") + adyenLog(AdyenLogLevel.DEBUG) { "launchNativeApi30 - redirect successful with native app" } true } catch (e: ActivityNotFoundException) { - Logger.d(TAG, "launchNativeApi30 - could not find native app to redirect with") + adyenLog(AdyenLogLevel.DEBUG) { "launchNativeApi30 - could not find native app to redirect with" } false } } @@ -163,10 +163,12 @@ class DefaultRedirectHandler : RedirectHandler { .setDefaultColorSchemeParams(defaultColors) .build() .launchUrl(context, uri) - Logger.d(TAG, "launchWithCustomTabs - redirect successful with custom tabs") + adyenLog(AdyenLogLevel.DEBUG) { "launchWithCustomTabs - redirect successful with custom tabs" } true } catch (e: ActivityNotFoundException) { - Logger.d(TAG, "launchWithCustomTabs - device doesn't support custom tabs or chrome is disabled") + adyenLog(AdyenLogLevel.DEBUG) { + "launchWithCustomTabs - device doesn't support custom tabs or chrome is disabled" + } false } } @@ -183,10 +185,10 @@ class DefaultRedirectHandler : RedirectHandler { .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setData(uri) context.startActivity(browserActivityIntent) - Logger.d(TAG, "launchBrowser - redirect successful with browser") + adyenLog(AdyenLogLevel.DEBUG) { "launchBrowser - redirect successful with browser" } true } catch (e: ActivityNotFoundException) { - Logger.d(TAG, "launchBrowser - could not do redirect on browser or there's no browser") + adyenLog(AdyenLogLevel.DEBUG) { "launchBrowser - could not do redirect on browser or there's no browser" } false } } @@ -200,8 +202,6 @@ class DefaultRedirectHandler : RedirectHandler { } companion object { - private val TAG = LogUtil.getTag() - private const val PAYLOAD_PARAMETER = "payload" private const val REDIRECT_RESULT_PARAMETER = "redirectResult" private const val PAYMENT_RESULT_PARAMETER = "PaRes" diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/AddressService.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/AddressService.kt index f38c7e22de..0f8ac517f5 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/AddressService.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/AddressService.kt @@ -12,16 +12,18 @@ import androidx.annotation.RestrictTo import com.adyen.checkout.core.internal.data.api.HttpClient import com.adyen.checkout.core.internal.data.api.getList import com.adyen.checkout.ui.core.internal.data.model.AddressItem +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class AddressService( private val httpClient: HttpClient, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) { suspend fun getCountries( shopperLocale: String - ): List = withContext(Dispatchers.IO) { + ): List = withContext(coroutineDispatcher) { httpClient.getList( path = "datasets/countries/$shopperLocale.json", responseSerializer = AddressItem.SERIALIZER, @@ -31,7 +33,7 @@ class AddressService( suspend fun getStates( shopperLocale: String, countryCode: String - ): List = withContext(Dispatchers.IO) { + ): List = withContext(coroutineDispatcher) { httpClient.getList( path = "datasets/states/$countryCode/$shopperLocale.json", responseSerializer = AddressItem.SERIALIZER, diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/DefaultAddressRepository.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/DefaultAddressRepository.kt index b6abc94b84..ea70733278 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/DefaultAddressRepository.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/data/api/DefaultAddressRepository.kt @@ -10,11 +10,12 @@ package com.adyen.checkout.ui.core.internal.data.api import androidx.annotation.RestrictTo import com.adyen.checkout.components.core.internal.util.bufferedChannel -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.core.internal.util.runSuspendCatching import com.adyen.checkout.ui.core.internal.data.model.AddressItem import com.adyen.checkout.ui.core.internal.ui.AddressSpecification +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -26,6 +27,7 @@ import java.util.Locale @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class DefaultAddressRepository( private val addressService: AddressService, + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : AddressRepository { private val statesChannel: Channel> = bufferedChannel() @@ -50,7 +52,7 @@ class DefaultAddressRepository( fetchStateList( shopperLocale, countryCode, - coroutineScope + coroutineScope, ) } } else { @@ -63,10 +65,10 @@ class DefaultAddressRepository( countryCode: String, coroutineScope: CoroutineScope ) { - coroutineScope.launch(Dispatchers.IO) { + coroutineScope.launch(coroutineDispatcher) { val states = getStates( shopperLocale = shopperLocale, - countryCode = countryCode + countryCode = countryCode, ).fold( onSuccess = { states -> if (states.isNotEmpty()) { @@ -74,7 +76,7 @@ class DefaultAddressRepository( } states }, - onFailure = { emptyList() } + onFailure = { emptyList() }, ) statesChannel.trySend(states) } @@ -86,15 +88,15 @@ class DefaultAddressRepository( } ?: run { fetchCountryList( shopperLocale, - coroutineScope + coroutineScope, ) } } private fun fetchCountryList(shopperLocale: Locale, coroutineScope: CoroutineScope) { - coroutineScope.launch(Dispatchers.IO) { + coroutineScope.launch(coroutineDispatcher) { val countries = getCountries( - shopperLocale = shopperLocale + shopperLocale = shopperLocale, ).fold( onSuccess = { countries -> if (countries.isNotEmpty()) { @@ -104,7 +106,7 @@ class DefaultAddressRepository( }, onFailure = { emptyList() - } + }, ) countriesChannel.trySend(countries) } @@ -113,7 +115,7 @@ class DefaultAddressRepository( private suspend fun getCountries( shopperLocale: Locale ): Result> = runSuspendCatching { - Logger.d(TAG, "getting country list") + adyenLog(AdyenLogLevel.DEBUG) { "getting country list" } return@runSuspendCatching addressService.getCountries(shopperLocale.toLanguageTag()) } @@ -121,17 +123,16 @@ class DefaultAddressRepository( shopperLocale: Locale, countryCode: String ): Result> = runSuspendCatching { - Logger.d(TAG, "getting state list for $countryCode") + adyenLog(AdyenLogLevel.DEBUG) { "getting state list for $countryCode" } return@runSuspendCatching addressService.getStates(shopperLocale.toLanguageTag(), countryCode) } companion object { - private val TAG = LogUtil.getTag() private val COUNTRIES_WITH_STATES = listOf( AddressSpecification.BR, AddressSpecification.CA, - AddressSpecification.US + AddressSpecification.US, ) private const val COUNTRIES_CACHE_KEY = "countries" } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/exception/PermissionRequestException.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/exception/PermissionRequestException.kt new file mode 100644 index 0000000000..4bf0297087 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/exception/PermissionRequestException.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 10/1/2024. + */ + +package com.adyen.checkout.ui.core.internal.exception + +import com.adyen.checkout.core.exception.CheckoutException + +/** + * Exception thrown when requested runtime permission is denied. + */ +class PermissionRequestException(errorMessage: String) : CheckoutException(errorMessage) diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/test/TestRedirectHandler.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/test/TestRedirectHandler.kt index 6c98c3b30e..9f4725f531 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/test/TestRedirectHandler.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/test/TestRedirectHandler.kt @@ -25,6 +25,7 @@ class TestRedirectHandler : RedirectHandler { var exception: ComponentException? = null private var timesLaunchRedirectCalled = 0 + private var timesRemoveOnRedirectListenerCalled = 0 override fun parseRedirectResult(data: Uri?): JSONObject { exception?.let { throw it } @@ -41,9 +42,17 @@ class TestRedirectHandler : RedirectHandler { override fun setOnRedirectListener(listener: () -> Unit) = Unit - override fun removeOnRedirectListener() = Unit + override fun removeOnRedirectListener() { + timesRemoveOnRedirectListenerCalled++ + } + + fun assertRemoveOnRedirectListenerCalled() = + assert(timesRemoveOnRedirectListenerCalled > 0) companion object { - val REDIRECT_RESULT = JSONObject().apply { put("redirect", "successful") } + val REDIRECT_RESULT = JSONObject().apply { + put("redirect", "successful") + put("returnUrlQueryString", "gpid=ajfbasljbfaljfe") + } } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressDelegate.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressDelegate.kt index 89440253bf..b0bf93f98c 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressDelegate.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressDelegate.kt @@ -9,7 +9,7 @@ package com.adyen.checkout.ui.core.internal.ui import androidx.annotation.RestrictTo -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData import kotlinx.coroutines.flow.Flow diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt index a02c988215..8e9069d759 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressFormUIState.kt @@ -15,7 +15,8 @@ import com.adyen.checkout.ui.core.internal.ui.model.AddressParams enum class AddressFormUIState { NONE, POSTAL_CODE, - FULL_ADDRESS; + FULL_ADDRESS, + LOOKUP; companion object { /** @@ -30,6 +31,7 @@ enum class AddressFormUIState { is AddressParams.FullAddress -> FULL_ADDRESS is AddressParams.PostalCode -> POSTAL_CODE is AddressParams.None -> NONE + is AddressParams.Lookup -> LOOKUP } } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressLookupDelegate.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressLookupDelegate.kt new file mode 100644 index 0000000000..6102ebe3f9 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressLookupDelegate.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupEvent +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +interface AddressLookupDelegate { + + val addressDelegate: AddressDelegate + + val addressLookupStateFlow: Flow + val addressLookupEventChannel: Channel + val addressLookupSubmitFlow: Flow + val addressLookupErrorPopupFlow: Flow + + fun initialize(coroutineScope: CoroutineScope, addressInputModel: AddressInputModel) + fun updateAddressLookupOptions(options: List) + fun setAddressLookupResult(addressLookupResult: AddressLookupResult) + fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) + fun onAddressQueryChanged(query: String) + fun onAddressLookupCompletion(lookupAddress: LookupAddress): Boolean + fun onManualEntryModeSelected() + fun submitAddress() + + fun clear() +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressSpecification.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressSpecification.kt index 7a5b36f765..0fbe3415c5 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressSpecification.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/AddressSpecification.kt @@ -223,7 +223,7 @@ enum class AddressSpecification( companion object { fun fromString(countryCode: String?): AddressSpecification { - return values().firstOrNull { it.name == countryCode } ?: DEFAULT + return entries.firstOrNull { it.name == countryCode } ?: DEFAULT } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/DefaultAddressLookupDelegate.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/DefaultAddressLookupDelegate.kt new file mode 100644 index 0000000000..c7d583ef18 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/DefaultAddressLookupDelegate.kt @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 22/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui + +import androidx.annotation.RestrictTo +import androidx.annotation.VisibleForTesting +import com.adyen.checkout.components.core.AddressLookupCallback +import com.adyen.checkout.components.core.AddressLookupResult +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.components.core.internal.util.bufferedChannel +import com.adyen.checkout.components.core.mapToAddressInputModel +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.ui.core.internal.data.api.AddressRepository +import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupEvent +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupInputData +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupState +import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData +import com.adyen.checkout.ui.core.internal.ui.model.AddressParams +import com.adyen.checkout.ui.core.internal.ui.view.LookupOption +import com.adyen.checkout.ui.core.internal.util.AddressFormUtils +import com.adyen.checkout.ui.core.internal.util.AddressValidationUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow +import java.util.Locale + +@Suppress("TooManyFunctions") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class DefaultAddressLookupDelegate( + private val addressRepository: AddressRepository, + private val shopperLocale: Locale +) : + AddressLookupDelegate, + AddressDelegate { + + private var coroutineScope: CoroutineScope? = null + + override val addressDelegate: AddressDelegate = this + + private var addressLookupCallback: AddressLookupCallback? = null + + private val addressLookupInputData = AddressLookupInputData() + + @VisibleForTesting + internal val mutableAddressLookupStateFlow = MutableStateFlow(AddressLookupState.Initial) + override val addressLookupStateFlow: Flow = mutableAddressLookupStateFlow + + private val currentAddressLookupState + get() = mutableAddressLookupStateFlow.value + + override val addressLookupEventChannel = bufferedChannel() + private val addressLookupEventFlow: Flow = addressLookupEventChannel.receiveAsFlow() + + override val addressOutputData: AddressOutputData + get() = _addressOutputDataFlow.value + + private val _addressOutputDataFlow = MutableStateFlow( + AddressValidationUtils.validateAddressInput( + addressLookupInputData.selectedAddress, + AddressFormUIState.LOOKUP, + emptyList(), + emptyList(), + false, + ), + ) + override val addressOutputDataFlow: Flow = _addressOutputDataFlow + + private val submitAddressChannel = bufferedChannel() + override val addressLookupSubmitFlow: Flow = submitAddressChannel.receiveAsFlow() + + private val addressLookupErrorPopupChannel = bufferedChannel() + override val addressLookupErrorPopupFlow: Flow = addressLookupErrorPopupChannel.receiveAsFlow() + + override fun initialize(coroutineScope: CoroutineScope, addressInputModel: AddressInputModel) { + this.coroutineScope = coroutineScope + addressLookupEventFlow + .onEach { addressLookupEvent -> + mutableAddressLookupStateFlow.emit( + makeAddressLookupState( + event = addressLookupEvent, + ), + ) + + if (addressLookupEvent is AddressLookupEvent.ErrorResult) { + addressLookupErrorPopupChannel.trySend(addressLookupEvent.message) + } + } + .launchIn(coroutineScope) + + subscribeToCountryList(coroutineScope) + subscribeToStateList(coroutineScope) + requestCountryList(coroutineScope) + + addressLookupEventChannel.trySend(AddressLookupEvent.Initialize(addressInputModel)) + } + + private fun subscribeToCountryList(coroutineScope: CoroutineScope) { + addressRepository.countriesFlow + .onEach { + adyenLog(AdyenLogLevel.DEBUG) { "country flow" } + val countryOptions = + AddressFormUtils.initializeCountryOptions(shopperLocale, AddressParams.Lookup(), it) + emitOutputData( + countryOptions = AddressFormUtils.markAddressListItemSelected( + list = countryOptions, + code = addressLookupInputData.selectedAddress.country, + ), + ) + } + .launchIn(coroutineScope) + } + + private fun requestCountryList(coroutineScope: CoroutineScope) { + adyenLog(AdyenLogLevel.DEBUG) { "requesting countries" } + addressRepository.getCountryList(shopperLocale, coroutineScope) + } + + private fun subscribeToStateList(coroutineScope: CoroutineScope) { + addressRepository.statesFlow + .onEach { + adyenLog(AdyenLogLevel.DEBUG) { "state flow $it" } + val stateOptions = AddressFormUtils.initializeStateOptions(it) + emitOutputData( + countryOptions = AddressFormUtils.markAddressListItemSelected( + addressOutputData.countryOptions, + addressLookupInputData.selectedAddress.country, + ), + stateOptions = AddressFormUtils.markAddressListItemSelected( + list = stateOptions, + code = addressLookupInputData.selectedAddress.stateOrProvince, + ), + ) + } + .launchIn(coroutineScope) + } + + private fun requestStatesList(countryCode: String) { + adyenLog(AdyenLogLevel.DEBUG) { "requesting states for $countryCode" } + coroutineScope?.let { + addressRepository.getStateList(shopperLocale, countryCode, it) + } ?: throw CheckoutException("Coroutine scope hasn't been initalized.") + } + + override fun onAddressQueryChanged(query: String) { + if (query.isEmpty()) { + addressLookupEventChannel.trySend(AddressLookupEvent.ClearQuery) + } else { + addressLookupEventChannel.trySend(AddressLookupEvent.Query(query)) + } + addressLookupCallback?.onQueryChanged(query) + } + + override fun onAddressLookupCompletion(lookupAddress: LookupAddress): Boolean { + val isLoading = addressLookupCallback?.onLookupCompletion(lookupAddress) ?: false + addressLookupEventChannel.trySend( + AddressLookupEvent.OptionSelected( + lookupAddress, + isLoading, + ), + ) + return isLoading + } + + override fun onManualEntryModeSelected() { + addressLookupEventChannel.trySend(AddressLookupEvent.Manual) + } + + override fun submitAddress() { + if (addressDelegate.addressOutputData.isValid) { + submitAddressChannel.trySend(addressLookupInputData.selectedAddress) + } else { + addressLookupEventChannel.trySend(AddressLookupEvent.InvalidUI) + } + } + + override fun updateAddressLookupOptions(options: List) { + addressLookupEventChannel.trySend(AddressLookupEvent.SearchResult(options)) + } + + override fun setAddressLookupResult(addressLookupResult: AddressLookupResult) { + when (addressLookupResult) { + is AddressLookupResult.Error -> { + addressLookupEventChannel.trySend( + AddressLookupEvent.ErrorResult(addressLookupResult.message), + ) + } + + is AddressLookupResult.Completed -> { + addressLookupEventChannel.trySend( + AddressLookupEvent.OptionSelected( + addressLookupResult.lookupAddress, + false, + ), + ) + } + } + } + + override fun setAddressLookupCallback(addressLookupCallback: AddressLookupCallback) { + this.addressLookupCallback = addressLookupCallback + } + + private fun makeAddressLookupState( + event: AddressLookupEvent, + ): AddressLookupState { + return when (event) { + is AddressLookupEvent.Initialize -> handleInitializeEvent(event) + is AddressLookupEvent.Query -> handleQueryEvent(event) + AddressLookupEvent.ClearQuery -> handleClearQueryEvent() + AddressLookupEvent.Manual -> handleManualEvent() + is AddressLookupEvent.SearchResult -> handleSearchResultEvent(event) + is AddressLookupEvent.OptionSelected -> handleOptionSelectedEvent(event) + is AddressLookupEvent.InvalidUI -> handleInvalidUIEvent() + is AddressLookupEvent.ErrorResult -> handleErrorEvent() + } + } + + private fun handleInitializeEvent(event: AddressLookupEvent.Initialize): AddressLookupState { + addressLookupInputData.selectedAddress.set(event.address) + return if (event.address.isEmpty) { + AddressLookupState.Initial + } else { + AddressLookupState.Form(event.address) + } + } + + private fun handleQueryEvent(event: AddressLookupEvent.Query): AddressLookupState { + addressLookupInputData.query = event.query + return AddressLookupState.Loading + } + + private fun handleClearQueryEvent(): AddressLookupState { + return if (!addressLookupInputData.selectedAddress.isEmpty) { + AddressLookupState.Form(addressLookupInputData.selectedAddress) + } else { + AddressLookupState.Initial + } + } + + private fun handleManualEvent(): AddressLookupState { + return if (currentAddressLookupState is AddressLookupState.Initial || + currentAddressLookupState is AddressLookupState.Error || + currentAddressLookupState is AddressLookupState.SearchResult + ) { + AddressLookupState.Form(null) + } else { + currentAddressLookupState + } + } + + private fun handleSearchResultEvent(event: AddressLookupEvent.SearchResult): AddressLookupState { + return if (currentAddressLookupState is AddressLookupState.Loading) { + if (event.addressLookupOptions.isEmpty()) { + AddressLookupState.Error(addressLookupInputData.query) + } else { + AddressLookupState.SearchResult( + addressLookupInputData.query, + event.addressLookupOptions.map { + LookupOption(lookupAddress = it, isLoading = false) + }, + ) + } + } else { + currentAddressLookupState + } + } + + private fun handleOptionSelectedEvent( + event: AddressLookupEvent.OptionSelected + ): AddressLookupState { + return if (currentAddressLookupState is AddressLookupState.SearchResult) { + if (event.loading) { + AddressLookupState.SearchResult( + (currentAddressLookupState as AddressLookupState.SearchResult).query, + (currentAddressLookupState as AddressLookupState.SearchResult).options.map { + LookupOption( + lookupAddress = it.lookupAddress, + isLoading = it.lookupAddress == event.lookupAddress, + ) + }, + ) + } else { + AddressLookupState.Form(event.lookupAddress.address.mapToAddressInputModel()) + } + } else { + currentAddressLookupState + } + } + + private fun handleInvalidUIEvent(): AddressLookupState { + return if (currentAddressLookupState is AddressLookupState.Form) { + AddressLookupState.InvalidUI + } else { + currentAddressLookupState + } + } + + private fun handleErrorEvent(): AddressLookupState { + return if (currentAddressLookupState is AddressLookupState.SearchResult) { + AddressLookupState.SearchResult( + (currentAddressLookupState as? AddressLookupState.SearchResult)?.query.orEmpty(), + (currentAddressLookupState as? AddressLookupState.SearchResult) + ?.options + ?.map { it.copy(isLoading = false) } + .orEmpty(), + ) + } else { + currentAddressLookupState + } + } + + override fun updateAddressInputData(update: AddressInputModel.() -> Unit) { + addressLookupInputData.selectedAddress.update() + requestStatesList(addressLookupInputData.selectedAddress.country) + emitOutputData() + } + + private fun createOutputData( + countryOptions: List, + stateOptions: List, + ): AddressOutputData { + return AddressValidationUtils.validateAddressInput( + addressInputModel = addressLookupInputData.selectedAddress, + addressFormUIState = AddressFormUIState.LOOKUP, + countryOptions = countryOptions, + stateOptions = stateOptions, + isOptional = false, + ) + } + + private fun emitOutputData( + countryOptions: List = addressOutputData.countryOptions, + stateOptions: List = addressOutputData.stateOptions, + ) { + _addressOutputDataFlow.tryEmit(createOutputData(countryOptions, stateOptions)) + } + + override fun clear() { + this.coroutineScope = null + } +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/ImageLoadingExtensions.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/ImageLoadingExtensions.kt index 67bb0223df..be404d8d34 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/ImageLoadingExtensions.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/ImageLoadingExtensions.kt @@ -16,11 +16,11 @@ import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.Environment import com.adyen.checkout.core.internal.ui.DefaultImageLoader import com.adyen.checkout.core.internal.ui.ImageLoader -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.R import kotlinx.coroutines.launch @@ -34,8 +34,6 @@ private val Context.imageLoader: ImageLoader return newImageLoader } -private val TAG = LogUtil.getTag() - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun ImageView.load( url: String, @@ -60,9 +58,9 @@ fun ImageView.load( url, onSuccess = { setImageBitmap(it) }, onError = { e -> - Logger.w(TAG, "Failed loading image for $url - ${e::class.simpleName}: ${e.message}") + adyenLog(AdyenLogLevel.WARN) { "Failed loading image for $url - ${e::class.simpleName}: ${e.message}" } setImageResource(errorFallback) - } + }, ) } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressInputModel.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressInputModel.kt deleted file mode 100644 index 7d687d2ca5..0000000000 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressInputModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2022 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by ozgur on 8/3/2022. - */ - -package com.adyen.checkout.ui.core.internal.ui.model - -import androidx.annotation.RestrictTo - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -data class AddressInputModel( - var postalCode: String = "", - var street: String = "", - var stateOrProvince: String = "", - var houseNumberOrName: String = "", - var apartmentSuite: String = "", - var city: String = "", - var country: String = "", -) { - /** - * Reset the data. - * - * Note: This method is called when country is changed and that's the reason [country] field - * does not get reset. - */ - fun reset() { - postalCode = "" - street = "" - stateOrProvince = "" - houseNumberOrName = "" - apartmentSuite = "" - city = "" - } -} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupEvent.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupEvent.kt new file mode 100644 index 0000000000..203c99d591 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +sealed class AddressLookupEvent { + data class Initialize(val address: AddressInputModel) : AddressLookupEvent() + data class Query(val query: String) : AddressLookupEvent() + object Manual : AddressLookupEvent() + object ClearQuery : AddressLookupEvent() + data class SearchResult(val addressLookupOptions: List) : AddressLookupEvent() + data class OptionSelected(val lookupAddress: LookupAddress, val loading: Boolean) : AddressLookupEvent() + object InvalidUI : AddressLookupEvent() + data class ErrorResult(val message: String? = null) : AddressLookupEvent() +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupInputData.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupInputData.kt new file mode 100644 index 0000000000..424ec98a03 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupInputData.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +data class AddressLookupInputData( + var query: String = "", + var selectedAddress: AddressInputModel = AddressInputModel() +) diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupState.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupState.kt new file mode 100644 index 0000000000..6a144727b2 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressLookupState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui.model + +import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel +import com.adyen.checkout.ui.core.internal.ui.view.LookupOption + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +sealed class AddressLookupState { + object Initial : AddressLookupState() + object Loading : AddressLookupState() + data class Form(val selectedAddress: AddressInputModel?) : AddressLookupState() + data class SearchResult(val query: String, val options: List) : AddressLookupState() + data class Error(val query: String) : AddressLookupState() + object InvalidUI : AddressLookupState() +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressOutputData.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressOutputData.kt index d6df4baeb2..89843aabcf 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressOutputData.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressOutputData.kt @@ -33,4 +33,16 @@ data class AddressOutputData( apartmentSuite.validation.isValid() && city.validation.isValid() && country.validation.isValid() + + override fun toString(): String { + return listOf( + street.value, + houseNumberOrName.value, + apartmentSuite.value, + postalCode.value, + city.value, + stateOrProvince.value, + country.value + ).filter { it.isNotBlank() }.joinToString(" ") + } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressParams.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressParams.kt index 4fa74fd081..a0f6566145 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressParams.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/model/AddressParams.kt @@ -41,6 +41,11 @@ sealed class AddressParams { val supportedCountryCodes: List = emptyList(), val addressFieldPolicy: AddressFieldPolicy ) : AddressParams() + + /** + * Address Lookup option will be shown as part of card component. + */ + class Lookup : AddressParams() } /** diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressFormInput.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressFormInput.kt index 57d855834a..5b977e64dc 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressFormInput.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressFormInput.kt @@ -207,10 +207,11 @@ class AddressFormInput @JvmOverloads constructor( } private fun updateCountries(countryList: List) { + val currentSelected = countryAdapter.getItem { it.selected } countryAdapter.setItems(countryList) val selectedCountry = countryList.firstOrNull { it.selected } val selectedSpecification = AddressSpecification.fromString(selectedCountry?.code) - if (selectedSpecification != currentSpec) { + if (selectedSpecification != currentSpec || currentSelected != selectedCountry) { currentSpec = selectedSpecification autoCompleteTextViewCountry.setText(selectedCountry?.name) populateFormFields(selectedSpecification) @@ -251,53 +252,54 @@ class AddressFormInput @JvmOverloads constructor( initCountryInput(addressSpecification.country.styleResId) initStreetInput( styleResId = addressSpecification.street.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) initHouseNumberInput( styleResId = addressSpecification.houseNumber.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) initApartmentSuiteInput( styleResId = addressSpecification.apartmentSuite.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) initPostalCodeInput( styleResId = addressSpecification.postalCode.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) initCityInput( styleResId = addressSpecification.city.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) initProvinceTerritoryInput( styleResId = addressSpecification.stateProvince.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) initStatesInput( styleResId = addressSpecification.stateProvince.getStyleResId( - isOptional = delegate.addressOutputData.isOptional - ) + isOptional = delegate.addressOutputData.isOptional, + ), ) } private fun initHeader() { textViewHeader.setLocalizedTextFromStyle( R.style.AdyenCheckout_AddressForm_HeaderTextAppearance, - localizedContext + localizedContext, ) } private fun initCountryInput(styleResId: Int) { textInputLayoutCountry?.setLocalizedHintFromStyle( styleResId, - localizedContext + localizedContext, ) + autoCompleteTextViewCountry.setText(delegate.addressOutputData.countryOptions.firstOrNull { it.selected }?.name) } private fun initStreetInput(styleResId: Int?) { diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressLookupOptionsAdapter.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressLookupOptionsAdapter.kt new file mode 100644 index 0000000000..00b2749d66 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressLookupOptionsAdapter.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.ui.core.databinding.AddressLookupOptionItemViewBinding + +internal class AddressLookupOptionsAdapter( + private val onItemClicked: (LookupAddress) -> Unit +) : + ListAdapter( + AddressLookupOptionDiffCallback, + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddressLookupOptionViewHolder { + val binding = AddressLookupOptionItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return AddressLookupOptionViewHolder(binding, onItemClicked) + } + + override fun onBindViewHolder(holder: AddressLookupOptionViewHolder, position: Int) { + holder.bindItem(currentList[position]) + } + + internal class AddressLookupOptionViewHolder( + private val binding: AddressLookupOptionItemViewBinding, + private val onItemClicked: (LookupAddress) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + fun bindItem(lookupOption: LookupOption) { + binding.root.setOnClickListener { + onItemClicked(lookupOption.lookupAddress) + } + binding.progressBar.isVisible = lookupOption.isLoading + binding.textViewAddressHeader.text = lookupOption.title + binding.textViewAddressDescription.text = lookupOption.subtitle + } + } + + object AddressLookupOptionDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: LookupOption, newItem: LookupOption): Boolean = + oldItem.lookupAddress.id == newItem.lookupAddress.id + + override fun areContentsTheSame(oldItem: LookupOption, newItem: LookupOption): Boolean = + oldItem == newItem + } +} + +data class LookupOption( + val lookupAddress: LookupAddress, + val isLoading: Boolean = false +) { + override fun toString(): String { + return listOf( + lookupAddress.address.street, + lookupAddress.address.houseNumberOrName, + lookupAddress.address.apartmentSuite, + lookupAddress.address.postalCode, + lookupAddress.address.city, + lookupAddress.address.stateOrProvince, + lookupAddress.address.country, + ).filter { !it.isNullOrBlank() }.joinToString(" ") + } + + val title + get() = lookupAddress.address.street.ifBlank { + toString() + } + + val subtitle + get() = if (lookupAddress.address.street.isBlank()) { + "" + } else { + toString() + } +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressLookupView.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressLookupView.kt new file mode 100644 index 0000000000..8a90c56623 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AddressLookupView.kt @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ozgur on 19/12/2023. + */ + +package com.adyen.checkout.ui.core.internal.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.SearchView.OnQueryTextListener +import androidx.annotation.RestrictTo +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.adyen.checkout.components.core.LookupAddress +import com.adyen.checkout.components.core.internal.ui.ComponentDelegate +import com.adyen.checkout.ui.core.R +import com.adyen.checkout.ui.core.databinding.AddressLookupViewBinding +import com.adyen.checkout.ui.core.internal.ui.AddressLookupDelegate +import com.adyen.checkout.ui.core.internal.ui.ComponentView +import com.adyen.checkout.ui.core.internal.ui.model.AddressLookupState +import com.adyen.checkout.ui.core.internal.util.formatStringWithHyperlink +import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle +import com.adyen.checkout.ui.core.internal.util.showKeyboard +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@Suppress("TooManyFunctions") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class AddressLookupView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + LinearLayout( + context, + attrs, + defStyleAttr, + ), + ComponentView { + + private val binding: AddressLookupViewBinding = AddressLookupViewBinding.inflate(LayoutInflater.from(context), this) + + private lateinit var localizedContext: Context + + private lateinit var addressLookupDelegate: AddressLookupDelegate + + private var addressLookupOptionsAdapter: AddressLookupOptionsAdapter? = null + + init { + orientation = VERTICAL + val padding = resources.getDimension(R.dimen.standard_margin).toInt() + setPadding(padding, padding, padding, padding) + } + + override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) { + require(delegate is AddressLookupDelegate) { "Unsupported delegate type" } + addressLookupDelegate = delegate + + this.localizedContext = localizedContext + initLocalizedStrings(localizedContext) + + observeDelegate(delegate, coroutineScope) + + initAddressLookupQuery() + initAddressFormInput(coroutineScope) + initAddressOptions() + initManualEntryFields() + initSubmitAddressButton() + } + + override fun highlightValidationErrors() { + binding.addressFormInput.highlightValidationErrors(false) + } + + private fun observeDelegate(delegate: AddressLookupDelegate, coroutineScope: CoroutineScope) { + delegate.addressLookupStateFlow + .onEach { outputDataChanged(it) } + .launchIn(coroutineScope) + + delegate.addressLookupErrorPopupFlow + .onEach { message -> + val errorMessage = + message ?: localizedContext.getString(R.string.component_error) + AlertDialog.Builder(context) + .setTitle(R.string.error_dialog_title) + .setMessage(errorMessage) + .setPositiveButton(R.string.error_dialog_button) { dialog, _ -> dialog.dismiss() } + .show() + } + .launchIn(coroutineScope) + } + + private fun initLocalizedStrings(localizedContext: Context) { + binding.addressFormInput.initLocalizedContext(localizedContext) + + binding.textViewInitialDisclaimer.setLocalizedTextFromStyle( + styleResId = R.style.AdyenCheckout_AddressLookup_InitialDisclaimer_Title, + localizedContext = localizedContext, + ) + + binding.textViewManualEntryInitial.text = + localizedContext.getString(R.string.checkout_address_lookup_initial_description) + .formatStringWithHyperlink("#") + + binding.textViewError.setLocalizedTextFromStyle( + styleResId = R.style.AdyenCheckout_AddressLookup_Empty_Title, + localizedContext = localizedContext, + ) + + binding.textViewManualEntryError.setLocalizedTextFromStyle( + styleResId = R.style.AdyenCheckout_AddressLookup_Empty_Description, + localizedContext = localizedContext, + formatHyperLink = true, + ) + } + + private fun initAddressLookupQuery() { + binding.textInputLayoutAddressLookupQuerySearch.apply { + setOnQueryTextListener( + object : OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + onQueryChanged(newText) + return true + } + }, + ) + setOnQueryTextFocusChangeListener { _, hasFocus -> + isSelected = hasFocus + } + requestFocus() + showKeyboard() + } + } + + private fun initAddressFormInput(coroutineScope: CoroutineScope) { + binding.addressFormInput.attachDelegate(addressLookupDelegate.addressDelegate, coroutineScope) + } + + private fun initAddressOptions() { + addressLookupOptionsAdapter = AddressLookupOptionsAdapter(::onAddressSelected) + addressLookupOptionsAdapter?.let { adapter -> + binding.recyclerViewAddressLookupOptions.adapter = adapter + } + } + + private fun initManualEntryFields() { + binding.textViewManualEntryError.setOnClickListener { + addressLookupDelegate.onManualEntryModeSelected() + } + + binding.textViewManualEntryInitial.setOnClickListener { + addressLookupDelegate.onManualEntryModeSelected() + } + + binding.buttonManualEntry.setOnClickListener { + addressLookupDelegate.onManualEntryModeSelected() + } + } + + private fun initSubmitAddressButton() { + binding.submitAddressButton.setOnClickListener { + addressLookupDelegate.submitAddress() + } + } + + private fun outputDataChanged(addressLookupState: AddressLookupState) { + when (addressLookupState) { + is AddressLookupState.Error -> handleErrorState(addressLookupState) + is AddressLookupState.Initial -> handleInitialState() + AddressLookupState.Loading -> handleLoadingState() + is AddressLookupState.Form -> handleFormState(addressLookupState) + is AddressLookupState.SearchResult -> handleSearchResultState(addressLookupState) + AddressLookupState.InvalidUI -> handleInvalidUIState() + } + } + + private fun handleErrorState(addressLookupState: AddressLookupState.Error) { + binding.recyclerViewAddressLookupOptions.isVisible = false + binding.textViewInitialDisclaimer.isVisible = false + binding.textViewError.isVisible = true + binding.textViewManualEntryError.isVisible = true + binding.textViewManualEntryInitial.isVisible = false + binding.addressFormInput.isVisible = false + binding.progressBar.isVisible = false + binding.buttonManualEntry.isVisible = false + binding.divider.isVisible = false + binding.submitAddressButton.isVisible = false + binding.textViewManualEntryError.text = + localizedContext.getString(R.string.checkout_address_lookup_empty_description, addressLookupState.query) + .formatStringWithHyperlink("#") + } + + private fun handleInitialState() { + binding.recyclerViewAddressLookupOptions.isVisible = false + binding.textViewInitialDisclaimer.isVisible = true + binding.textViewError.isVisible = false + binding.textViewManualEntryError.isVisible = false + binding.textViewManualEntryInitial.isVisible = true + binding.addressFormInput.isVisible = false + binding.progressBar.isVisible = false + binding.buttonManualEntry.isVisible = false + binding.divider.isVisible = false + binding.submitAddressButton.isVisible = false + } + + private fun handleLoadingState() { + binding.recyclerViewAddressLookupOptions.isVisible = false + binding.textViewInitialDisclaimer.isVisible = false + binding.textViewError.isVisible = false + binding.textViewManualEntryError.isVisible = false + binding.textViewManualEntryInitial.isVisible = false + binding.addressFormInput.isVisible = false + binding.progressBar.isVisible = true + binding.buttonManualEntry.isVisible = false + binding.divider.isVisible = false + binding.submitAddressButton.isVisible = false + } + + private fun handleFormState(addressLookupState: AddressLookupState.Form) { + binding.recyclerViewAddressLookupOptions.isVisible = false + binding.textViewInitialDisclaimer.isVisible = false + binding.textViewError.isVisible = false + binding.textViewManualEntryError.isVisible = false + binding.textViewManualEntryInitial.isVisible = false + binding.addressFormInput.isVisible = true + binding.progressBar.isVisible = false + binding.buttonManualEntry.isVisible = false + binding.divider.isVisible = false + binding.submitAddressButton.isVisible = true + addressLookupDelegate.addressDelegate.updateAddressInputData { + if (addressLookupState.selectedAddress == null) { + this.resetAll() + } else { + this.set(addressLookupState.selectedAddress) + } + } + } + + private fun handleSearchResultState(addressLookupState: AddressLookupState.SearchResult) { + binding.recyclerViewAddressLookupOptions.isVisible = true + binding.textViewInitialDisclaimer.isVisible = false + binding.textViewError.isVisible = false + binding.textViewManualEntryError.isVisible = false + binding.textViewManualEntryInitial.isVisible = false + binding.addressFormInput.isVisible = false + binding.progressBar.isVisible = false + binding.buttonManualEntry.isVisible = true + binding.divider.isVisible = true + binding.submitAddressButton.isVisible = false + setAddressOptions(addressLookupState.options) + } + + private fun handleInvalidUIState() { + binding.recyclerViewAddressLookupOptions.isVisible = false + binding.textViewInitialDisclaimer.isVisible = false + binding.textViewError.isVisible = false + binding.textViewManualEntryError.isVisible = false + binding.textViewManualEntryInitial.isVisible = false + binding.addressFormInput.isVisible = true + binding.progressBar.isVisible = false + binding.buttonManualEntry.isVisible = false + binding.divider.isVisible = false + binding.submitAddressButton.isVisible = true + highlightValidationErrors() + } + + private fun onQueryChanged(query: String) { + addressLookupDelegate.onAddressQueryChanged(query) + } + + private fun setAddressOptions(options: List) { + if (addressLookupOptionsAdapter == null) { + initAddressOptions() + } + addressLookupOptionsAdapter?.submitList(options) + } + + private fun onAddressSelected(lookupAddress: LookupAddress) { + addressLookupDelegate.onAddressLookupCompletion(lookupAddress) + } + + override fun getView(): View = this +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AdyenTextInputEditText.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AdyenTextInputEditText.kt index 85dd73af8f..839ccdf188 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AdyenTextInputEditText.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/AdyenTextInputEditText.kt @@ -35,7 +35,7 @@ open class AdyenTextInputEditText @JvmOverloads constructor( this.addTextChangedListener(textWatcher) } - fun setOnChangeListener(listener: Listener) { + fun setOnChangeListener(listener: Listener?) { this.listener = listener } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/SocialSecurityNumberInput.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/SocialSecurityNumberInput.kt index 89496b8d40..b9302472b1 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/SocialSecurityNumberInput.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/ui/view/SocialSecurityNumberInput.kt @@ -31,12 +31,16 @@ class SocialSecurityNumberInput constructor( init { enforceMaxInputLength( - SocialSecurityNumberUtils.CNPJ_DIGIT_LIMIT + SocialSecurityNumberUtils.CNPJ_MASK_SEPARATORS.size + SocialSecurityNumberUtils.CNPJ_DIGIT_LIMIT + SocialSecurityNumberUtils.CNPJ_MASK_SEPARATORS.size, ) inputType = InputType.TYPE_CLASS_NUMBER keyListener = DigitsKeyListener.getInstance(SUPPORTED_CHARS) } + fun setSocialSecurityNumber(socialSecurityNumber: String) { + setText(SocialSecurityNumberUtils.formatInput(socialSecurityNumber)) + } + override fun afterTextChanged(editable: Editable) { val original = editable.toString() val formatted = SocialSecurityNumberUtils.formatInput(original) diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressFormUtils.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressFormUtils.kt index 164317ac0e..fdd58dc709 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressFormUtils.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressFormUtils.kt @@ -74,6 +74,11 @@ object AddressFormUtils { ) markAddressListItemSelected(mapToListItem(filteredCountryList), defaultCountryCode) } + + is AddressParams.Lookup -> { + mapToListItem(countryList) + } + else -> emptyList() } } @@ -136,6 +141,7 @@ object AddressFormUtils { city = addressOutputData.city.value.ifEmpty { Address.ADDRESS_NULL_PLACEHOLDER }, country = addressOutputData.country.value, ) + AddressFormUIState.POSTAL_CODE -> Address( postalCode = addressOutputData.postalCode.value, street = Address.ADDRESS_NULL_PLACEHOLDER, @@ -144,6 +150,7 @@ object AddressFormUtils { city = Address.ADDRESS_NULL_PLACEHOLDER, country = Address.ADDRESS_COUNTRY_NULL_PLACEHOLDER, ) + else -> null } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressValidationUtils.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressValidationUtils.kt index b67ff3e32b..b9d5dd31da 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressValidationUtils.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/AddressValidationUtils.kt @@ -9,12 +9,12 @@ package com.adyen.checkout.ui.core.internal.util import androidx.annotation.RestrictTo +import com.adyen.checkout.components.core.internal.ui.model.AddressInputModel import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.ui.core.R import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.AddressSpecification -import com.adyen.checkout.ui.core.internal.ui.model.AddressInputModel import com.adyen.checkout.ui.core.internal.ui.model.AddressListItem import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData @@ -33,18 +33,20 @@ object AddressValidationUtils { isOptional: Boolean, ): AddressOutputData { return when (addressFormUIState) { - AddressFormUIState.FULL_ADDRESS -> validateAddressInput( + AddressFormUIState.FULL_ADDRESS, AddressFormUIState.LOOKUP -> validateAddressInput( addressInputModel, isOptional, countryOptions, - stateOptions + stateOptions, ) + AddressFormUIState.POSTAL_CODE -> validatePostalCode( addressInputModel, isOptional, countryOptions, - stateOptions + stateOptions, ) + else -> makeValidEmptyAddressOutput(addressInputModel) } } @@ -66,7 +68,7 @@ object AddressValidationUtils { country = FieldState(country, Validation.Valid), isOptional = isOptional, countryOptions = countryOptions, - stateOptions = stateOptions + stateOptions = stateOptions, ) } } @@ -89,7 +91,7 @@ object AddressValidationUtils { country = validateAddressField(country, spec.country.isRequired && !isOptional), isOptional = isOptional, countryOptions = countryOptions, - stateOptions = stateOptions + stateOptions = stateOptions, ) } } @@ -109,7 +111,7 @@ object AddressValidationUtils { country = FieldState(country, Validation.Valid), isOptional = true, countryOptions = emptyList(), - stateOptions = emptyList() + stateOptions = emptyList(), ) } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ImageSaver.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ImageSaver.kt new file mode 100644 index 0000000000..99738f0ae3 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ImageSaver.kt @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 8/1/2024. + */ + +package com.adyen.checkout.ui.core.internal.util + +import android.Manifest +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Bitmap.CompressFormat +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import android.provider.MediaStore.Images.Media +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission +import androidx.annotation.RestrictTo +import androidx.core.content.ContextCompat +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.exception.CheckoutException +import com.adyen.checkout.core.internal.ui.PermissionHandler +import com.adyen.checkout.core.internal.util.adyenLog +import com.adyen.checkout.ui.core.R +import com.adyen.checkout.ui.core.internal.exception.PermissionRequestException +import com.adyen.checkout.ui.core.internal.util.PermissionHandlerResult.PERMISSION_GRANTED +import com.adyen.checkout.ui.core.internal.util.PermissionHandlerResult.PERMISSION_REQUEST_NOT_HANDLED +import com.google.android.material.color.MaterialColors +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedInputStream +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.net.MalformedURLException +import java.net.URL + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +class ImageSaver( + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +) { + + @Suppress("LongParameterList") + suspend fun saveImageFromView( + context: Context, + permissionHandler: PermissionHandler, + view: View, + @ColorInt backgroundColor: Int? = null, + fileName: String? = null, + fileRelativePath: String? = null, + ): Result { + val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + backgroundColor?.let { color -> canvas.drawColor(color) } + ?: view.background?.draw(canvas) + ?: run { + val defaultColor = ContextCompat.getColor(context, R.color.white) + val defaultBackgroundColor = MaterialColors.getColor(context, R.attr.colorSurface, defaultColor) + canvas.drawColor(defaultBackgroundColor) + } + + view.draw(canvas) + + return saveImageFromBitmap(context, permissionHandler, bitmap, fileName, fileRelativePath) + } + + suspend fun saveImageFromUrl( + context: Context, + permissionHandler: PermissionHandler, + imageUrl: String, + fileName: String? = null, + fileRelativePath: String? = null, + ): Result = withContext(dispatcher) { + val url = imageUrl.toURL() ?: return@withContext Result.failure(CheckoutException("Malformed URL")) + + return@withContext try { + val inputStream = url.openStream() + val bufferedInputStream = BufferedInputStream(inputStream) + val bitmap = BitmapFactory.decodeStream(bufferedInputStream) + + saveImageFromBitmap(context, permissionHandler, bitmap, fileName, fileRelativePath) + } catch (exception: IOException) { + Result.failure(CheckoutException("Malformed URL: $exception")) + } + } + + private suspend fun saveImageFromBitmap( + context: Context, + permissionHandler: PermissionHandler, + bitmap: Bitmap, + fileName: String? = null, + fileRelativePath: String? = null, + ): Result { + val timestamp = System.currentTimeMillis() + val imageName = fileName ?: timestamp.toString() + val imagePath = fileRelativePath ?: Environment.DIRECTORY_DOWNLOADS + val contentValues = ContentValues().apply { + put(Media.MIME_TYPE, "image/png") + put(Media.DATE_ADDED, timestamp) + put(Media.DATE_TAKEN, timestamp) + put(Media.DISPLAY_NAME, imageName) + put(Media.RELATIVE_PATH, imagePath) + } + + val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + saveImageApi29AndAbove(context, bitmap, contentValues) + } else { + saveImageApi28AndBelow(context, permissionHandler, bitmap, contentValues) + } + bitmap.recycle() + return result + } + + @RequiresApi(Build.VERSION_CODES.Q) + private suspend fun saveImageApi29AndAbove( + context: Context, + bitmap: Bitmap, + contentValues: ContentValues, + ): Result = withContext(dispatcher) { + contentValues.put(Media.IS_PENDING, true) + + val uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + ?: return@withContext Result.failure(CheckoutException("Error when saving Bitmap as an image: URI is null")) + + return@withContext try { + val outputStream = context.contentResolver.openOutputStream(uri) + ?: return@withContext Result.failure(CheckoutException("Output stream is null")) + + contentValues.put(Media.IS_PENDING, false) + context.contentResolver.update(uri, contentValues, null, null) + + bitmap.compress(CompressFormat.PNG, PNG_QUALITY, outputStream) + outputStream.close() + + adyenLog(AdyenLogLevel.DEBUG) { "Bitmap successfully saved as an image" } + Result.success(Unit) + } catch (e: FileNotFoundException) { + Result.failure(CheckoutException("File not found: ", e)) + } + } + + @SuppressLint("MissingPermission") + private suspend fun saveImageApi28AndBelow( + context: Context, + permissionHandler: PermissionHandler, + bitmap: Bitmap, + contentValues: ContentValues, + ): Result = withContext(dispatcher) { + when (permissionHandler.checkPermission(context, REQUIRED_PERMISSION)) { + PERMISSION_GRANTED -> saveImageApi28AndBelowWhenPermissionGranted(bitmap, contentValues) + PERMISSION_REQUEST_NOT_HANDLED -> { + adyenLog(AdyenLogLevel.ERROR) { "Permission request not handled" } + Result.failure(PermissionRequestException("Permission request not handled")) + } + + else -> Result.failure(PermissionRequestException("The $REQUIRED_PERMISSION permission is denied")) + } + } + + @RequiresPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + private suspend fun saveImageApi28AndBelowWhenPermissionGranted( + bitmap: Bitmap, + contentValues: ContentValues + ): Result = withContext(dispatcher) { + val fileName = contentValues.getAsString(Media.DISPLAY_NAME) + val filePath = contentValues.getAsString(Media.RELATIVE_PATH) + val imageFileFolder = + Environment.getExternalStoragePublicDirectory(filePath) + if (!imageFileFolder.exists()) { + imageFileFolder.mkdirs() + } + val imageFile = File(imageFileFolder, fileName) + + return@withContext try { + val outputStream = FileOutputStream(imageFile) + + bitmap.compress(CompressFormat.PNG, PNG_QUALITY, outputStream) + outputStream.close() + + adyenLog(AdyenLogLevel.DEBUG) { "Bitmap successfully saved as an image" } + Result.success(Unit) + } catch (e: FileNotFoundException) { + Result.failure(CheckoutException("File not found: ", e)) + } catch (e: SecurityException) { + Result.failure(CheckoutException("Security violation: ", e)) + } + } + + private fun String.toURL(): URL? { + return try { + URL(this) + } catch (e: MalformedURLException) { + adyenLog(AdyenLogLevel.ERROR) { "Failed to convert String to URL: $e" } + null + } + } + + companion object { + private const val PNG_QUALITY = 100 + private const val REQUIRED_PERMISSION = Manifest.permission.WRITE_EXTERNAL_STORAGE + } +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt index bba9e854d0..9d7705fa28 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt @@ -16,8 +16,8 @@ import android.os.Build import androidx.annotation.RestrictTo import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent -import com.adyen.checkout.core.internal.util.LogUtil -import com.adyen.checkout.core.internal.util.Logger +import com.adyen.checkout.core.AdyenLogLevel +import com.adyen.checkout.core.internal.util.adyenLog @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) class PdfOpener { @@ -28,7 +28,7 @@ class PdfOpener { if (open(context, uri)) return if (openInBrowser(context, uri)) return - Logger.e(TAG, "Couldn't open pdf with url: $uri") + adyenLog(AdyenLogLevel.ERROR) { "Couldn't open pdf with url: $uri" } error("Couldn't open pdf with url: $uri") } @@ -53,10 +53,10 @@ class PdfOpener { return try { context.startActivity(nativeAppIntent) - Logger.d(TAG, "Successfully opened pdf in external app") + adyenLog(AdyenLogLevel.DEBUG) { "Successfully opened pdf in external app" } true - } catch (ex: ActivityNotFoundException) { - Logger.d(TAG, "Couldn't open pdf in external app", ex) + } catch (e: ActivityNotFoundException) { + adyenLog(AdyenLogLevel.DEBUG, e) { "Couldn't open pdf in external app" } false } } @@ -74,10 +74,10 @@ class PdfOpener { .build() .launchUrl(context, uri) - Logger.d(TAG, "Successfully opened pdf in custom tab") + adyenLog(AdyenLogLevel.DEBUG) { "Successfully opened pdf in custom tab" } true } catch (e: ActivityNotFoundException) { - Logger.d(TAG, "Couldn't open pdf in custom tab", e) + adyenLog(AdyenLogLevel.DEBUG, e) { "Couldn't open pdf in custom tab" } false } } @@ -91,15 +91,11 @@ class PdfOpener { .setData(uri) context.startActivity(browserActivityIntent) - Logger.d(TAG, "Successfully opened pdf in browser") + adyenLog(AdyenLogLevel.DEBUG) { "Successfully opened pdf in browser" } true } catch (e: ActivityNotFoundException) { - Logger.d(TAG, "Couldn't open pdf in browser", e) + adyenLog(AdyenLogLevel.DEBUG, e) { "Couldn't open pdf in browser" } false } } - - companion object { - private val TAG = LogUtil.getTag() - } } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PermissionHandlerExtension.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PermissionHandlerExtension.kt new file mode 100644 index 0000000000..b4cfb62e4e --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PermissionHandlerExtension.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by ararat on 9/1/2024. + */ + +package com.adyen.checkout.ui.core.internal.util + +import android.content.Context +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import com.adyen.checkout.core.PermissionHandlerCallback +import com.adyen.checkout.core.internal.ui.PermissionHandler +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +internal suspend fun PermissionHandler.checkPermission( + context: Context, + requiredPermission: String +): PermissionHandlerResult = suspendCancellableCoroutine { continuation -> + if (ContextCompat.checkSelfPermission(context, requiredPermission) == PackageManager.PERMISSION_GRANTED) { + continuation.resume(PermissionHandlerResult.PERMISSION_GRANTED) + return@suspendCancellableCoroutine + } + + requestPermission( + context = context, + requiredPermission = requiredPermission, + callback = object : PermissionHandlerCallback { + override fun onPermissionGranted(requestedPermission: String) { + if (requestedPermission == requiredPermission) { + continuation.resume(PermissionHandlerResult.PERMISSION_GRANTED) + } else { + continuation.resume(PermissionHandlerResult.WRONG_PERMISSION) + } + } + + override fun onPermissionDenied(requestedPermission: String) { + continuation.resume(PermissionHandlerResult.PERMISSION_DENIED) + } + + override fun onPermissionRequestNotHandled(requestedPermission: String) { + continuation.resume(PermissionHandlerResult.PERMISSION_REQUEST_NOT_HANDLED) + } + }, + ) +} + +internal enum class PermissionHandlerResult { + PERMISSION_GRANTED, + PERMISSION_DENIED, + PERMISSION_REQUEST_NOT_HANDLED, + WRONG_PERMISSION, +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ViewExtensions.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ViewExtensions.kt index 6693b902d3..25359a37ec 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ViewExtensions.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ViewExtensions.kt @@ -61,6 +61,13 @@ internal fun String.formatStringWithHyperlink(replacementToken: String = "%#"): } } +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +fun String.formatFullStringWithHyperLink(): CharSequence { + return SpannableString(this).apply { + setSpan(URLSpan(""), 0, this.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } +} + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) fun TextInputLayout.showError(error: String) { isErrorEnabled = true diff --git a/ui-core/src/main/res/drawable/address_lookup_search_border.xml b/ui-core/src/main/res/drawable/address_lookup_search_border.xml new file mode 100644 index 0000000000..c1776b590f --- /dev/null +++ b/ui-core/src/main/res/drawable/address_lookup_search_border.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/ui-core/src/main/res/drawable/address_lookup_search_input_arrow.xml b/ui-core/src/main/res/drawable/address_lookup_search_input_arrow.xml new file mode 100644 index 0000000000..c8e71a95ba --- /dev/null +++ b/ui-core/src/main/res/drawable/address_lookup_search_input_arrow.xml @@ -0,0 +1,6 @@ + + + diff --git a/ui-core/src/main/res/layout/address_lookup_option_item_view.xml b/ui-core/src/main/res/layout/address_lookup_option_item_view.xml new file mode 100644 index 0000000000..693fd15c8c --- /dev/null +++ b/ui-core/src/main/res/layout/address_lookup_option_item_view.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + diff --git a/ui-core/src/main/res/layout/address_lookup_view.xml b/ui-core/src/main/res/layout/address_lookup_view.xml new file mode 100644 index 0000000000..f2415d540c --- /dev/null +++ b/ui-core/src/main/res/layout/address_lookup_view.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + +