diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml
index 68bf4dd447..5ea50a663f 100644
--- a/.github/workflows/check_pr.yml
+++ b/.github/workflows/check_pr.yml
@@ -17,13 +17,13 @@ jobs:
# https://github.com/marketplace/actions/checkout
- uses: actions/checkout@v3
- # Setup Java 11
+ # Setup Java 17
# https://github.com/marketplace/actions/setup-java-jdk
- - name: Set up JDK 11
+ - name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: 11
+ java-version: 17
cache: 'gradle'
- name: Grant execute permission for gradlew
diff --git a/.github/workflows/check_release.yml b/.github/workflows/check_release.yml
index 205a12cb26..305fd606b6 100644
--- a/.github/workflows/check_release.yml
+++ b/.github/workflows/check_release.yml
@@ -17,13 +17,13 @@ jobs:
# https://github.com/marketplace/actions/checkout
- uses: actions/checkout@v3
- # Setup Java 11
+ # Setup Java 17
# https://github.com/marketplace/actions/setup-java-jdk
- - name: Set up JDK 11
+ - name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: 11
+ java-version: 17
cache: 'gradle'
- name: Grant execute permission for gradlew
diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml
index 9b0e6d9508..b840f6ed45 100644
--- a/.github/workflows/publish_docs.yml
+++ b/.github/workflows/publish_docs.yml
@@ -18,7 +18,7 @@ jobs:
run: ./gradlew dokkaHtmlMultiModule --no-daemon
- name: Deploy GitHub Pages
- uses: JamesIves/github-pages-deploy-action@v4.4.1
+ uses: JamesIves/github-pages-deploy-action@v4.4.3
with:
BRANCH: gh-pages
FOLDER: build/docs/
diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml
index 52f445438c..6802194b69 100644
--- a/.github/workflows/publish_release.yml
+++ b/.github/workflows/publish_release.yml
@@ -16,11 +16,11 @@ jobs:
with:
ref: main
- - name: Set up JDK 11
+ - name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
- java-version: 11
+ java-version: 17
cache: 'gradle'
- name: Grant execute permission for gradlew
diff --git a/.github/workflows/update_verification_metadata.yml b/.github/workflows/update_verification_metadata.yml
new file mode 100644
index 0000000000..d62dc27399
--- /dev/null
+++ b/.github/workflows/update_verification_metadata.yml
@@ -0,0 +1,45 @@
+name: Update verification metadata
+
+on:
+ push:
+ branches:
+ - 'renovate/**'
+ paths:
+ - 'dependencies.gradle'
+
+jobs:
+ gradle-update-verification-metadata:
+ # https://github.com/actions/virtual-environments/
+ runs-on: ubuntu-latest
+
+ steps:
+ # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
+ # https://github.com/marketplace/actions/checkout
+ - uses: actions/checkout@v3
+
+ # Setup Java 17
+ # https://github.com/marketplace/actions/setup-java-jdk
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'zulu'
+ java-version: 17
+ cache: 'gradle'
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ # Run gradlew check
+ - name: Gradle update verification metadata
+ run: ./gradlew --write-verification-metadata sha256 build --no-daemon
+
+ - name: Commit
+ run: |
+ git config --local user.email 'action@github.com'
+ git config --local user.name 'GitHub Action'
+ git add .
+ git commit -am 'Update verification metadata'
+
+ - name: Push
+ uses: ad-m/github-push-action@master
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ branch: ${{ github.ref }}
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
index d00dc19f45..90e4d11a40 100644
--- a/.idea/copyright/profiles_settings.xml
+++ b/.idea/copyright/profiles_settings.xml
@@ -1,7 +1,3 @@
-
-
-
-
-
-
+
+
\ No newline at end of file
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 92ec934aa8..99c85911d7 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
@@ -50,7 +50,7 @@ internal class SubmitFingerprintRepository internal constructor(
}
else -> {
Logger.e(TAG, "submitFingerprint: unexpected response $response")
- throw IllegalStateException("Failed to retrieve 3DS2 fingerprint result")
+ error("Failed to retrieve 3DS2 fingerprint result")
}
}
}
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 58e3b706a8..4fab618e76 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
@@ -40,11 +40,11 @@ import com.adyen.checkout.components.core.internal.util.viewModelFactory
import com.adyen.checkout.core.internal.data.api.HttpClientFactory
import com.adyen.checkout.ui.core.internal.DefaultRedirectHandler
import com.adyen.threeds2.ThreeDS2Service
-import com.adyen.threeds2.parameters.ChallengeParameters
import kotlinx.coroutines.Dispatchers
+class Adyen3DS2ComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class Adyen3DS2ComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) : ActionComponentProvider {
@@ -79,11 +79,9 @@ class Adyen3DS2ComponentProvider(
savedStateHandle: SavedStateHandle,
application: Application,
): Adyen3DS2Delegate {
- val defaultThreeDSRequestorAppURL = ChallengeParameters.getEmbeddedRequestorAppURL(application)
val componentParams = componentParamsMapper.mapToParams(
adyen3DS2Configuration = configuration,
sessionParams = null,
- defaultThreeDSRequestorAppURL = defaultThreeDSRequestorAppURL,
)
val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
val submitFingerprintService = SubmitFingerprintService(httpClient)
diff --git a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/Adyen3DS2ViewProvider.kt b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/Adyen3DS2ViewProvider.kt
index cffc844ba6..60a200a1b3 100644
--- a/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/Adyen3DS2ViewProvider.kt
+++ b/3ds2/src/main/java/com/adyen/checkout/adyen3ds2/internal/ui/Adyen3DS2ViewProvider.kt
@@ -24,7 +24,7 @@ internal object Adyen3DS2ViewProvider : ViewProvider {
defStyleAttr: Int
): ComponentView = when (viewType) {
Adyen3DS2ComponentViewType -> PaymentInProgressView(context, attrs, defStyleAttr)
- else -> throw IllegalStateException("Unsupported view type")
+ else -> error("Unsupported view type")
}
}
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 331bc504a0..ff31cf219c 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
@@ -197,11 +197,11 @@ internal class DefaultAdyen3DS2Delegate(
val fingerprintToken = FingerprintToken.SERIALIZER.deserialize(fingerprintJson)
val configParameters = AdyenConfigParameters.Builder(
- /* directoryServerId = */
+ // directoryServerId
fingerprintToken.directoryServerId,
- /* directoryServerPublicKey = */
+ // directoryServerPublicKey
fingerprintToken.directoryServerPublicKey,
- /* directoryServerRootCertificates = */
+ // directoryServerRootCertificates
fingerprintToken.directoryServerRootCertificates,
).build()
@@ -211,6 +211,9 @@ internal class DefaultAdyen3DS2Delegate(
}
coroutineScope.launch(defaultDispatcher + 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")
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 b82afa0def..3f81e86b59 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
@@ -24,5 +24,5 @@ internal data class Adyen3DS2ComponentParams(
override val isCreatedByDropIn: Boolean,
override val amount: Amount,
val uiCustomization: UiCustomization?,
- val threeDSRequestorAppURL: String,
+ val threeDSRequestorAppURL: String?,
) : ComponentParams
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 73a838a3eb..414d1060b9 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
@@ -20,17 +20,14 @@ internal class Adyen3DS2ComponentParamsMapper(
fun mapToParams(
adyen3DS2Configuration: Adyen3DS2Configuration,
sessionParams: SessionParams?,
- defaultThreeDSRequestorAppURL: String,
): Adyen3DS2ComponentParams {
return adyen3DS2Configuration
- .mapToParamsInternal(defaultThreeDSRequestorAppURL)
+ .mapToParamsInternal()
.override(overrideComponentParams)
.override(sessionParams ?: overrideSessionParams)
}
- private fun Adyen3DS2Configuration.mapToParamsInternal(
- defaultThreeDSRequestorAppURL: String,
- ): Adyen3DS2ComponentParams {
+ private fun Adyen3DS2Configuration.mapToParamsInternal(): Adyen3DS2ComponentParams {
return Adyen3DS2ComponentParams(
shopperLocale = shopperLocale,
environment = environment,
@@ -39,7 +36,7 @@ internal class Adyen3DS2ComponentParamsMapper(
isCreatedByDropIn = false,
amount = amount,
uiCustomization = uiCustomization,
- threeDSRequestorAppURL = threeDSRequestorAppURL ?: defaultThreeDSRequestorAppURL,
+ threeDSRequestorAppURL = threeDSRequestorAppURL,
)
}
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 6eef3c770e..fcda299224 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
@@ -91,7 +91,7 @@ internal class DefaultAdyen3DS2DelegateTest(
observerRepository = ActionObserverRepository(),
savedStateHandle = SavedStateHandle(),
componentParams = Adyen3DS2ComponentParamsMapper(null, null)
- .mapToParams(configuration, null, "embeddedRequestorAppUrl"),
+ .mapToParams(configuration, null),
submitFingerprintRepository = submitFingerprintRepository,
paymentDataRepository = paymentDataRepository,
adyen3DS2Serializer = adyen3DS2Serializer,
@@ -526,7 +526,7 @@ internal class DefaultAdyen3DS2DelegateTest(
}
override fun getProgressView(p0: Activity?): ProgressDialog {
- throw IllegalStateException("This method should not be used")
+ error("This method should not be used")
}
override fun close() = Unit
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 376df0a642..cb0f29589b 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
@@ -13,7 +13,7 @@ import com.adyen.checkout.components.core.Amount
import com.adyen.checkout.components.core.internal.ui.model.GenericComponentParams
import com.adyen.checkout.core.Environment
import com.adyen.threeds2.customization.UiCustomization
-import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.Locale
@@ -25,11 +25,11 @@ internal class Adyen3DS2ComponentParamsMapperTest {
.build()
val params = Adyen3DS2ComponentParamsMapper(null, null)
- .mapToParams(adyen3DS2Configuration, null, TEST_REQUESTOR_APP_URL)
+ .mapToParams(adyen3DS2Configuration, null)
val expected = getAdyen3DS2ComponentParams()
- Assertions.assertEquals(expected, params)
+ assertEquals(expected, params)
}
@Test
@@ -43,14 +43,14 @@ internal class Adyen3DS2ComponentParamsMapperTest {
.build()
val params = Adyen3DS2ComponentParamsMapper(null, null)
- .mapToParams(adyen3DS2Configuration, null, TEST_REQUESTOR_APP_URL)
+ .mapToParams(adyen3DS2Configuration, null)
val expected = getAdyen3DS2ComponentParams(
uiCustomization = uiCustomization,
threeDSRequestorAppURL = testUrl,
)
- Assertions.assertEquals(expected, params)
+ assertEquals(expected, params)
}
@Test
@@ -73,7 +73,7 @@ internal class Adyen3DS2ComponentParamsMapperTest {
)
val params = Adyen3DS2ComponentParamsMapper(overrideParams, null)
- .mapToParams(adyen3DS2Configuration, null, TEST_REQUESTOR_APP_URL)
+ .mapToParams(adyen3DS2Configuration, null)
val expected = getAdyen3DS2ComponentParams(
shopperLocale = Locale.GERMAN,
@@ -87,7 +87,7 @@ internal class Adyen3DS2ComponentParamsMapperTest {
),
)
- Assertions.assertEquals(expected, params)
+ assertEquals(expected, params)
}
private fun getAdyen3DS2ConfigurationBuilder() = Adyen3DS2Configuration.Builder(
@@ -105,7 +105,7 @@ internal class Adyen3DS2ComponentParamsMapperTest {
isCreatedByDropIn: Boolean = false,
amount: Amount = Amount.EMPTY,
uiCustomization: UiCustomization? = null,
- threeDSRequestorAppURL: String = TEST_REQUESTOR_APP_URL,
+ threeDSRequestorAppURL: String? = null,
) = Adyen3DS2ComponentParams(
shopperLocale = shopperLocale,
environment = environment,
@@ -120,6 +120,5 @@ internal class Adyen3DS2ComponentParamsMapperTest {
companion object {
private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty"
private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty"
- private const val TEST_REQUESTOR_APP_URL = "TEST_REQUESTOR_APP_URL"
}
}
diff --git a/README.md b/README.md
index 8e4528de96..377c2882bb 100644
--- a/README.md
+++ b/README.md
@@ -29,11 +29,11 @@ If you are upgrading from 3.x.x to a current release, check out our [migration g
Import the Component module for the Payment Method you want to use by adding it to your `build.gradle` file.
For example, for the Drop-in solution you should add:
```groovy
-implementation "com.adyen.checkout:drop-in:4.11.0"
+implementation "com.adyen.checkout:drop-in:4.12.0"
```
For a Credit Card component you should add:
```groovy
-implementation "com.adyen.checkout:card:4.11.0"
+implementation "com.adyen.checkout:card:4.12.0"
```
### Client Key
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 2d9a0e7e6e..c502834354 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -8,73 +8,50 @@
[//]: # ( # Deprecated)
[//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object)
-## Breaking Changes
+⚠️ This is an alpha release. Don't use it to accept payments in your live environment.
-- For Drop in, you can no longer get the result using `onActivityResult()`. Drop-in now uses the [Activity Result API](https://developer.android.com/training/basics/intents/result) instead.
-- For Components, you can no longer use `requiresView()` for action Component providers.
-- Restructured packages and moved classes. If you're upgrading, you only need to re-import the them because most classes names haven't changed.
-- All public classes that should not be directly used are now marked as internal.
-- You now must configure `environment`. The default value is no longer **TEST**.
-- Build configuration: [`compileSdkVersion` and `targetSdkVersion`](https://developer.android.com/about/versions/11/setup-sdk#update-build): **33**.
-- Dependency versions:
- | Name | Version |
- |--------------------------------------------------------------------------------------------------------|------------|
- | [Android Gradle plugin](https://developer.android.com/build/releases/gradle-plugin) | **7.4.2** |
- | [Kotlin Gradle plugin](https://plugins.gradle.org/plugin/org.jetbrains.kotlin.android) | **1.8.21** |
- | [Appcompat](https://developer.android.com/jetpack/androidx/releases/appcompat) | **1.6.1** |
- | [Kotlin coroutines](https://kotlinlang.org/docs/coroutines-overview.html) | **1.6.4** |
- | [AndroidX Fragment](https://developer.android.com/jetpack/androidx/releases/fragment) | **1.5.7** |
- | [AndroidX Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) | **2.5.1** |
- | [AndroidX Recyclerview](https://developer.android.com/jetpack/androidx/releases/recyclerview) | **1.3.0** |
- | [AndroidX Constraintlayout](https://developer.android.com/jetpack/androidx/releases/constraintlayout) | **2.1.4** |
- | [Material Design](https://m2.material.io/) | **1.8.0** |
-
-## Removed
-- `requiresConfiguration()` in action Component providers. For all Components, configuration is optional.
-- `CardConfiguration.Builder.setAddressVisibility()`. Use `CardConfiguration.Builder.setAddressConfiguration()` instead.
-- `Environment.LIVE`. Use the same live environment as your backend instead. You can find that value in your Customer Area.
-- `saveState()` and `restoreState()` in action components. The component will automatically handle the state now.
-- `DropInServiceResult.Action` constructor from JSON string. Use the constructor with the `Action` and `Action.SERIALIZER` instead.
+## Breaking changes
+- All classes in `com.adyen.checkout.action` are now in `com.adyen.checkout.action.core`. If you import the classes, you must update import statements.
+- For Components integrations, each payment component no longer handles 3D Secure 2 and WeChat Pay actions. To handle the actions, you must add dependencies for each action:
+ ```Groovy
+ implementation 'com.adyen.checkout:3ds2:YOUR_VERSION'
+ implementation 'com.adyen.checkout:wechatpay:YOUR_VERSION'
+ ```
+ Exceptions: `CardComponent` and `BcmcComponent` can handle the 3D Secure 2 action. They don't require the additional dependencies.
## New
-- Sessions flow using the single `/sessions` request is now supported.
-- For Components:
- - Payment method Components now handle actions. You no longer need a payment Component and action Components for a payment method with additional actions.
- - The `GenericActionComponent` that can handle all action types. You no longer need to implement separate Components for redirects and 3D Secure 2 authentication, for example.
- - A **Pay** button that you can configure to be hidden.
- - The `submit()` method that can be used to add your own pay/submit button.
- - You can now add `amount` to the configuration to show it on the pay/submit button.
- - The `onSubmit()` event that gets emitted when the shopper pays.
-- When the shopper is redirected back from an external app or website, an intermediate view with a loading spinner and a **Cancel** button now shows. The shopper can select to cancel the redirect back to your app.
-- Localisation for the Portuguese (Portugal) language.
-- Payment methods:
- - [ACH Direct Debit](https://docs.adyen.com/payment-methods/ach-direct-debit). [Payment method type](https://docs.adyen.com/payment-methods/payment-method-types): **ach**.
- - [DuitNow](https://docs.adyen.com/payment-methods/duitnow). Payment method type: **duitnow**.
- - [Open banking](https://docs.adyen.com/payment-methods/open-banking). Payment method type: **paybybank**.
- - [Online banking Czech Republic](https://docs.adyen.com/payment-methods/online-banking-czech-republic). Payment method type: **onlineBanking_CZ**.
- - [Online banking Slovakia](https://docs.adyen.com/payment-methods/online-banking-slovakia). Payment method type: **onlineBanking_SK**.
- - [Pay Now](https://docs.adyen.com/payment-methods/paynow). Payment method type: **paynow**.
- - [PromptPay](https://docs.adyen.com/payment-methods/promptpay). Payment method type: **promptpay**.
- - [UPI](https://docs.adyen.com/payment-methods/upi):
- - UPI Collect: The shopper pays by entering their virtual payment address (VPA). Payment method type: **upi_collect**.
- - UPI QR: The shopper pays by scanning a QR code. Payment method type: **upi_qr**.
-- Express payment methods like PayPal and Klarna. These payment methods don't require the shopper to enter their payment details before they pay. Use `InstantPaymentComponent`.
+- Payment method: [Boleto Bancario](https://docs.adyen.com/payment-methods/boleto-bancario). Payment method type: **boletobancario**.
+- [Jetpack Compose](https://developer.android.com/jetpack/compose) compatibility.
+ - For Drop-in, use the `drop-in-compose` module.
+ - For Components, use the `components-compose` module.
+- For cards, the `brand` attribute is now included in the `paymentMethod` object for all cards. Previously, it was just included for co-branded ones.
+- You can now safely exclude unnecessary third-party dependencies. Do this by excluding the Adyen Checkout module that includes the third-party dependency. For example:
+ ```Groovy
+ implementation('com.adyen.checkout:drop-in:YOUR_VERSION') {
+ exclude group: 'com.adyen.checkout', module: '3ds2'
+ exclude group: 'com.adyen.checkout', module: 'wechatpay'
+ }
+ ```
+ Make sure that you don't include a payment method that corresponds to the module that you exclude.
-## Changed
-- For cards:
- - The supported brand logo icons now show below the card number input field.
- - US Debit brand logo icons no longer show.
-- For Drop-in, values set in `DropInConfiguration` now override conflicting configurations for individual payment methods.
-- For Google Pay, you can now set `GooglePayConfiguration.merchantAccount` to override the `gatewayMerchantId` configured in your Customer Area. For Advanced flow, this is the `paymentMethod.configuration.gatewayMerchantId` parameter in the `/paymentMethods` response.
-- For Components, when a payment method doesn't require input from the shopper, the Component that launches automatically returns the `onSubmit()` callback. For example, for the stored cards without a CVC input field.
-- For gift cards and partial payments, you must now implement `onBalanceCheck()` and `onRequestOrder()` to launch payment methods with an order and make [partial payments](https://docs.adyen.com/online-payments/partial-payments).
+- For Google Pay, new configurations in `GooglePayConfiguration`:
+ | Function | Description |
+ |-------------------------------|-------------------------------------------|
+ | `setAllowCreditCards` | Specify if you allow credit cards. |
+ | `setAssuranceDetailsRequired` | Specify if you require assurance details. |
## Improved
-- You can now instantiate more than one instance of the same Component within the same lifecycle. Passing the `key` parameter to the Component provider `get()` method. For example, you can show cards and stored cards on the same screen.
-- For Components, you no longer need to handle duplicate events such as submit callbacks or errors with because they're only emitted once. [Flows](https://developer.android.com/kotlin/flow) are now used instead of [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
-- More UI theme customization options like dark mode.
-- The expiry date input field now has more specific validation rules and error messages.
-- The email address input field now has more specific validation rules.
+- Email input validation.
## Fixed
-- The redirect flow on Android 11.
+- `@RestrictTo` annotations no longer cause false [lint check warnings](https://developer.android.com/studio/write/lint).
+
+## Changed
+- Dependency versions:
+ | Name | Version |
+ |--------------------------------------------------------------------------------------------------------|-------------------------------|
+ | [Android Gradle plugin](https://developer.android.com/build/releases/gradle-plugin) | **8.0.2** (requires Java 17) |
+ | [Kotlin Gradle plugin](https://plugins.gradle.org/plugin/org.jetbrains.kotlin.android) | **1.8.22** |
+ | [AndroidX Fragment](https://developer.android.com/jetpack/androidx/releases/fragment) | **1.6.0** |
+ | [Material Design](https://m2.material.io/) | **1.9.0** |
+ | [Google Pay](https://developers.google.com/pay/api/android/support/release-notes#jun-22) | **19.2.0** |
diff --git a/ach/build.gradle b/ach/build.gradle
index c455a50473..68059afd78 100644
--- a/ach/build.gradle
+++ b/ach/build.gradle
@@ -33,7 +33,7 @@ android {
}
dependencies {
- api project(':action')
+ api project(':action-core')
api project(':ui-core')
api project(':cse')
api project(':sessions-core')
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 6161a963f2..9e7ede9561 100644
--- a/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitComponent.kt
+++ b/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitComponent.kt
@@ -14,9 +14,9 @@ import androidx.lifecycle.viewModelScope
import com.adyen.checkout.ach.internal.provider.ACHDirectDebitComponentProvider
import com.adyen.checkout.ach.internal.ui.ACHDirectDebitDelegate
import com.adyen.checkout.ach.internal.ui.DefaultACHDirectDebitDelegate
-import com.adyen.checkout.action.internal.ActionHandlingComponent
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.ActionHandlingComponent
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.PaymentMethodTypes
import com.adyen.checkout.components.core.internal.ButtonComponent
import com.adyen.checkout.components.core.internal.ComponentEventHandler
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 fe788cd043..724e28e95b 100644
--- a/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitConfiguration.kt
+++ b/ach/src/main/java/com/adyen/checkout/ach/ACHDirectDebitConfiguration.kt
@@ -9,8 +9,8 @@
package com.adyen.checkout.ach
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.ActionHandlingPaymentMethodConfigurationBuilder
+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.internal.ButtonConfiguration
import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
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 025b3feaf6..a99fa77b0c 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
@@ -20,8 +20,8 @@ import com.adyen.checkout.ach.ACHDirectDebitConfiguration
import com.adyen.checkout.ach.internal.ui.DefaultACHDirectDebitDelegate
import com.adyen.checkout.ach.internal.ui.StoredACHDirectDebitDelegate
import com.adyen.checkout.ach.internal.ui.model.ACHDirectDebitComponentParamsMapper
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider
import com.adyen.checkout.components.core.ComponentCallback
import com.adyen.checkout.components.core.Order
import com.adyen.checkout.components.core.PaymentMethod
@@ -59,8 +59,9 @@ 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
+class ACHDirectDebitComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ACHDirectDebitComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) :
@@ -68,22 +69,26 @@ class ACHDirectDebitComponentProvider(
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,
diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/view/ACHDirectDebitView.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/view/ACHDirectDebitView.kt
index 712cf65d4e..0433eff209 100644
--- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/view/ACHDirectDebitView.kt
+++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/view/ACHDirectDebitView.kt
@@ -54,7 +54,7 @@ internal class ACHDirectDebitView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is ACHDirectDebitDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is ACHDirectDebitDelegate) { "Unsupported delegate type" }
this.delegate = delegate
this.localizedContext = localizedContext
initLocalizedStrings(localizedContext)
diff --git a/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitComponentTest.kt b/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitComponentTest.kt
index 2218f4bf9f..3c768da1d5 100644
--- a/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitComponentTest.kt
+++ b/ach/src/test/java/com/adyen/checkout/ach/ACHDirectDebitComponentTest.kt
@@ -13,8 +13,8 @@ import androidx.lifecycle.viewModelScope
import com.adyen.checkout.ach.internal.ui.ACHDirectDebitComponentViewType
import com.adyen.checkout.ach.internal.ui.DefaultACHDirectDebitDelegate
import com.adyen.checkout.ach.internal.ui.StoredACHDirectDebitDelegate
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+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.test.TestDispatcherExtension
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 13f9add612..c08be20fa6 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
@@ -162,7 +162,8 @@ internal class ACHDirectDebitComponentParamsMapperTest {
val sessionParams = SessionParams(
enableStoreDetails = sessionsValue,
installmentOptions = null,
- amount = null
+ amount = null,
+ returnUrl = "",
)
val params = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(
@@ -210,7 +211,8 @@ internal class ACHDirectDebitComponentParamsMapperTest {
sessionParams = SessionParams(
enableStoreDetails = null,
installmentOptions = null,
- amount = sessionsValue
+ amount = sessionsValue,
+ returnUrl = "",
)
)
diff --git a/action-core/.gitignore b/action-core/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/action-core/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/action-core/build.gradle b/action-core/build.gradle
new file mode 100644
index 0000000000..a668c0c9e5
--- /dev/null
+++ b/action-core/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * 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 oscars on 10/5/2023.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id 'kotlin-parcelize'
+}
+
+ext.mavenArtifactId = "action-core"
+ext.mavenArtifactName = "Adyen Checkout Action Core component"
+ext.mavenArtifactDescription = "Adyen Checkout Action Core module."
+
+apply from: "${rootDir}/config/gradle/sharedTasks.gradle"
+
+android {
+ namespace 'com.adyen.checkout.action'
+ compileSdkVersion compile_sdk_version
+
+ defaultConfig {
+ minSdkVersion min_sdk_version
+ targetSdkVersion target_sdk_version
+ versionCode version_code
+ versionName version_name
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ consumerProguardFiles "consumer-rules.pro"
+ }
+}
+
+dependencies {
+ // Checkout
+ compileOnly project(':3ds2')
+ api project(':await')
+ api project(':qr-code')
+ api project(':redirect')
+ compileOnly project(':wechatpay')
+ api project(':voucher')
+
+ //Tests
+ testImplementation project(':3ds2')
+ testImplementation project(':test-core')
+ testImplementation project(':wechatpay')
+ testImplementation testLibraries.json
+ testImplementation testLibraries.junit5
+ testImplementation testLibraries.mockito
+ testImplementation testLibraries.kotlinCoroutines
+}
diff --git a/action-core/consumer-rules.pro b/action-core/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/action/src/main/java/com/adyen/checkout/action/GenericActionComponent.kt b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt
similarity index 90%
rename from action/src/main/java/com/adyen/checkout/action/GenericActionComponent.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt
index 5f99bc92d6..232b752ab7 100644
--- a/action/src/main/java/com/adyen/checkout/action/GenericActionComponent.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionComponent.kt
@@ -5,14 +5,14 @@
*
* Created by josephj on 23/8/2022.
*/
-package com.adyen.checkout.action
+package com.adyen.checkout.action.core
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.adyen.checkout.action.internal.ActionHandlingComponent
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+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.internal.ActionComponent
import com.adyen.checkout.components.core.internal.ActionComponentEvent
import com.adyen.checkout.components.core.internal.ActionComponentEventHandler
diff --git a/action/src/main/java/com/adyen/checkout/action/GenericActionConfiguration.kt b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt
similarity index 96%
rename from action/src/main/java/com/adyen/checkout/action/GenericActionConfiguration.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt
index 05476706e7..901d353bad 100644
--- a/action/src/main/java/com/adyen/checkout/action/GenericActionConfiguration.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/GenericActionConfiguration.kt
@@ -6,12 +6,12 @@
* Created by josephj on 23/8/2022.
*/
-package com.adyen.checkout.action
+package com.adyen.checkout.action.core
import android.content.Context
import androidx.annotation.RestrictTo
-import com.adyen.checkout.action.GenericActionConfiguration.Builder
-import com.adyen.checkout.action.internal.ActionHandlingConfigurationBuilder
+import com.adyen.checkout.action.core.GenericActionConfiguration.Builder
+import com.adyen.checkout.action.core.internal.ActionHandlingConfigurationBuilder
import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration
import com.adyen.checkout.await.AwaitConfiguration
import com.adyen.checkout.components.core.Amount
@@ -47,7 +47,6 @@ class GenericActionConfiguration private constructor(
internal inline fun getConfigurationForAction(): T? {
val actionClass = T::class.java
if (availableActionConfigs.containsKey(actionClass)) {
- @Suppress("UNCHECKED_CAST")
return availableActionConfigs[actionClass] as T
}
return null
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingComponent.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingComponent.kt
similarity index 96%
rename from action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingComponent.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingComponent.kt
index 2dc8c0281b..7c5e7e8bdf 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingComponent.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingComponent.kt
@@ -6,7 +6,7 @@
* Created by oscars on 11/11/2022.
*/
-package com.adyen.checkout.action.internal
+package com.adyen.checkout.action.core.internal
import android.app.Activity
import android.content.Intent
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingConfigurationBuilder.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt
similarity index 84%
rename from action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingConfigurationBuilder.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt
index e82a5656a1..2f841fc99b 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingConfigurationBuilder.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingConfigurationBuilder.kt
@@ -1,4 +1,12 @@
-package com.adyen.checkout.action.internal
+/*
+ * 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 10/5/2023.
+ */
+
+package com.adyen.checkout.action.core.internal
import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration
import com.adyen.checkout.await.AwaitConfiguration
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt
similarity index 93%
rename from action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt
index 6b0ea79a9d..db2032318f 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ActionHandlingPaymentMethodConfigurationBuilder.kt
@@ -1,8 +1,16 @@
-package com.adyen.checkout.action.internal
+/*
+ * 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 10/5/2023.
+ */
+
+package com.adyen.checkout.action.core.internal
import android.content.Context
import androidx.annotation.RestrictTo
-import com.adyen.checkout.action.GenericActionConfiguration
+import com.adyen.checkout.action.core.GenericActionConfiguration
import com.adyen.checkout.adyen3ds2.Adyen3DS2Configuration
import com.adyen.checkout.await.AwaitConfiguration
import com.adyen.checkout.components.core.internal.BaseConfigurationBuilder
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/DefaultActionHandlingComponent.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt
similarity index 89%
rename from action/src/main/java/com/adyen/checkout/action/internal/DefaultActionHandlingComponent.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt
index c94520d279..1e91b80682 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/DefaultActionHandlingComponent.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/DefaultActionHandlingComponent.kt
@@ -6,13 +6,13 @@
* Created by oscars on 11/11/2022.
*/
-package com.adyen.checkout.action.internal
+package com.adyen.checkout.action.core.internal
import android.app.Activity
import android.content.Intent
import androidx.annotation.RestrictTo
-import com.adyen.checkout.action.GenericActionComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.GenericActionComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.components.core.internal.ui.ComponentDelegate
import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/provider/ActionComponentExtensions.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt
similarity index 65%
rename from action/src/main/java/com/adyen/checkout/action/internal/provider/ActionComponentExtensions.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt
index 0e25a5fd4b..70adbf359d 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/provider/ActionComponentExtensions.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/ActionComponentExtensions.kt
@@ -6,17 +6,27 @@
* Created by josephj on 19/9/2022.
*/
-package com.adyen.checkout.action.internal.provider
+package com.adyen.checkout.action.core.internal.provider
import com.adyen.checkout.adyen3ds2.Adyen3DS2Component
import com.adyen.checkout.await.AwaitComponent
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.components.core.internal.provider.ActionComponentProvider
+import com.adyen.checkout.core.internal.util.runCompileOnly
import com.adyen.checkout.qrcode.QRCodeComponent
import com.adyen.checkout.redirect.RedirectComponent
import com.adyen.checkout.voucher.VoucherComponent
import com.adyen.checkout.wechatpay.WeChatPayActionComponent
+private val allActionProviders = listOfNotNull(
+ runCompileOnly { Adyen3DS2Component.PROVIDER },
+ runCompileOnly { AwaitComponent.PROVIDER },
+ runCompileOnly { QRCodeComponent.PROVIDER },
+ runCompileOnly { RedirectComponent.PROVIDER },
+ runCompileOnly { VoucherComponent.PROVIDER },
+ runCompileOnly { WeChatPayActionComponent.PROVIDER },
+)
+
/**
* @param action The action to be handled
*
@@ -25,13 +35,5 @@ import com.adyen.checkout.wechatpay.WeChatPayActionComponent
internal fun getActionProviderFor(
action: Action
): ActionComponentProvider<*, *, *>? {
- val allActionProviders = listOf(
- RedirectComponent.PROVIDER,
- Adyen3DS2Component.PROVIDER,
- WeChatPayActionComponent.PROVIDER,
- AwaitComponent.PROVIDER,
- QRCodeComponent.PROVIDER,
- VoucherComponent.PROVIDER
- )
return allActionProviders.firstOrNull { it.canHandleAction(action) }
}
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/provider/GenericActionComponentProvider.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt
similarity index 89%
rename from action/src/main/java/com/adyen/checkout/action/internal/provider/GenericActionComponentProvider.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt
index 7f8f126d89..6615b310a2 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/provider/GenericActionComponentProvider.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/provider/GenericActionComponentProvider.kt
@@ -6,7 +6,7 @@
* Created by josephj on 23/8/2022.
*/
-package com.adyen.checkout.action.internal.provider
+package com.adyen.checkout.action.core.internal.provider
import android.app.Application
import androidx.annotation.RestrictTo
@@ -15,12 +15,12 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
-import com.adyen.checkout.action.GenericActionComponent
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.ActionDelegateProvider
-import com.adyen.checkout.action.internal.ui.DefaultGenericActionDelegate
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+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.components.core.ActionComponentCallback
import com.adyen.checkout.components.core.action.Action
import com.adyen.checkout.components.core.action.AwaitAction
@@ -39,8 +39,9 @@ import com.adyen.checkout.components.core.internal.ui.model.GenericComponentPara
import com.adyen.checkout.components.core.internal.util.get
import com.adyen.checkout.components.core.internal.util.viewModelFactory
+class GenericActionComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class GenericActionComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
) : ActionComponentProvider {
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/ui/ActionDelegateProvider.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt
similarity index 80%
rename from action/src/main/java/com/adyen/checkout/action/internal/ui/ActionDelegateProvider.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt
index 88a837feea..4da2b00642 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/ui/ActionDelegateProvider.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/ActionDelegateProvider.kt
@@ -6,11 +6,11 @@
* Created by oscars on 24/8/2022.
*/
-package com.adyen.checkout.action.internal.ui
+package com.adyen.checkout.action.core.internal.ui
import android.app.Application
import androidx.lifecycle.SavedStateHandle
-import com.adyen.checkout.action.GenericActionConfiguration
+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
@@ -28,6 +28,7 @@ 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.core.exception.CheckoutException
+import com.adyen.checkout.core.internal.util.runCompileOnly
import com.adyen.checkout.qrcode.QRCodeConfiguration
import com.adyen.checkout.qrcode.internal.provider.QRCodeComponentProvider
import com.adyen.checkout.redirect.RedirectConfiguration
@@ -56,6 +57,7 @@ internal class ActionDelegateProvider(
application
)
}
+
is QrCodeAction -> {
QRCodeComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate(
getConfigurationForAction(configuration),
@@ -63,6 +65,7 @@ internal class ActionDelegateProvider(
application
)
}
+
is RedirectAction -> {
RedirectComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate(
getConfigurationForAction(configuration),
@@ -70,6 +73,7 @@ internal class ActionDelegateProvider(
application
)
}
+
is BaseThreeds2Action -> {
Adyen3DS2ComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate(
getConfigurationForAction(configuration),
@@ -77,6 +81,7 @@ internal class ActionDelegateProvider(
application
)
}
+
is VoucherAction -> {
VoucherComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate(
getConfigurationForAction(configuration),
@@ -84,6 +89,7 @@ internal class ActionDelegateProvider(
application
)
}
+
is SdkAction<*> -> {
WeChatPayActionComponentProvider(overrideComponentParams, overrideSessionParams).getDelegate(
getConfigurationForAction(configuration),
@@ -91,6 +97,7 @@ internal class ActionDelegateProvider(
application
)
}
+
else -> throw CheckoutException("Can't find delegate for action: ${action.type}")
}
}
@@ -109,20 +116,45 @@ internal class ActionDelegateProvider(
val clientKey = configuration.clientKey
val builder: BaseConfigurationBuilder<*, *> = when (T::class) {
- AwaitConfiguration::class -> AwaitConfiguration.Builder(shopperLocale, environment, clientKey)
- RedirectConfiguration::class -> RedirectConfiguration.Builder(shopperLocale, environment, clientKey)
- QRCodeConfiguration::class -> QRCodeConfiguration.Builder(shopperLocale, environment, clientKey)
- Adyen3DS2Configuration::class -> Adyen3DS2Configuration.Builder(shopperLocale, environment, clientKey)
- WeChatPayActionConfiguration::class -> WeChatPayActionConfiguration.Builder(
+ runCompileOnly { AwaitConfiguration::class } -> AwaitConfiguration.Builder(
+ shopperLocale,
+ environment,
+ clientKey
+ )
+
+ runCompileOnly { RedirectConfiguration::class } -> RedirectConfiguration.Builder(
+ shopperLocale,
+ environment,
+ clientKey
+ )
+
+ runCompileOnly { QRCodeConfiguration::class } -> QRCodeConfiguration.Builder(
shopperLocale,
environment,
clientKey
)
- VoucherConfiguration::class -> VoucherConfiguration.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}")
}
- @Suppress("UNCHECKED_CAST")
return builder.build() as T
}
}
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/ui/DefaultGenericActionDelegate.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt
similarity index 94%
rename from action/src/main/java/com/adyen/checkout/action/internal/ui/DefaultGenericActionDelegate.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt
index c81cf8a6ac..f14ae234b8 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/ui/DefaultGenericActionDelegate.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/DefaultGenericActionDelegate.kt
@@ -6,13 +6,13 @@
* Created by josephj on 19/9/2022.
*/
-package com.adyen.checkout.action.internal.ui
+package com.adyen.checkout.action.core.internal.ui
import android.app.Activity
import android.content.Intent
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.SavedStateHandle
-import com.adyen.checkout.action.GenericActionConfiguration
+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.action.Action
@@ -30,6 +30,7 @@ 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.runCompileOnly
import com.adyen.checkout.ui.core.internal.ui.ComponentViewType
import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate
import kotlinx.coroutines.CoroutineScope
@@ -95,7 +96,7 @@ internal class DefaultGenericActionDelegate(
// Initially handleAction is called with a fingerprint action then with a challenge action.
// 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 (_delegate is Adyen3DS2Delegate && action is Threeds2ChallengeAction) {
+ if (isOld3DS2Flow(action)) {
Logger.d(TAG, "Continuing the handling of 3ds2 challenge with old flow.")
} else {
val delegate = actionDelegateProvider.getDelegate(
@@ -117,6 +118,10 @@ internal class DefaultGenericActionDelegate(
delegate.handleAction(action, activity)
}
+ private fun isOld3DS2Flow(action: Action): Boolean {
+ return runCompileOnly { _delegate is Adyen3DS2Delegate && action is Threeds2ChallengeAction } ?: false
+ }
+
private fun observeExceptions(delegate: ActionDelegate) {
Logger.d(TAG, "Observing exceptions")
delegate.exceptionFlow
diff --git a/action/src/main/java/com/adyen/checkout/action/internal/ui/GenericActionDelegate.kt b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt
similarity index 93%
rename from action/src/main/java/com/adyen/checkout/action/internal/ui/GenericActionDelegate.kt
rename to action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt
index 4329370aa6..31e3c583cf 100644
--- a/action/src/main/java/com/adyen/checkout/action/internal/ui/GenericActionDelegate.kt
+++ b/action-core/src/main/java/com/adyen/checkout/action/core/internal/ui/GenericActionDelegate.kt
@@ -6,7 +6,7 @@
* Created by josephj on 19/9/2022.
*/
-package com.adyen.checkout.action.internal.ui
+package com.adyen.checkout.action.core.internal.ui
import androidx.annotation.RestrictTo
import com.adyen.checkout.components.core.internal.ui.ActionDelegate
diff --git a/action/src/test/java/com/adyen/checkout/action/GenericActionComponentTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt
similarity index 95%
rename from action/src/test/java/com/adyen/checkout/action/GenericActionComponentTest.kt
rename to action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt
index e4fe145189..efc6412557 100644
--- a/action/src/test/java/com/adyen/checkout/action/GenericActionComponentTest.kt
+++ b/action-core/src/test/java/com/adyen/checkout/action/core/GenericActionComponentTest.kt
@@ -6,13 +6,13 @@
* Created by josephj on 19/12/2022.
*/
-package com.adyen.checkout.action
+package com.adyen.checkout.action.core
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
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
diff --git a/action/src/test/java/com/adyen/checkout/action/internal/ui/TestActionDelegate.kt b/action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt
similarity index 99%
rename from action/src/test/java/com/adyen/checkout/action/internal/ui/TestActionDelegate.kt
rename to action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt
index 23831e877c..dffb1aaa8c 100644
--- a/action/src/test/java/com/adyen/checkout/action/internal/ui/TestActionDelegate.kt
+++ b/action-core/src/test/java/com/adyen/checkout/action/core/TestActionDelegate.kt
@@ -6,7 +6,7 @@
* Created by josephj on 21/9/2022.
*/
-package com.adyen.checkout.action.internal.ui
+package com.adyen.checkout.action.core
import android.app.Activity
import android.content.Intent
diff --git a/action/src/test/java/com/adyen/checkout/action/internal/ui/DefaultGenericActionDelegateTest.kt b/action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt
similarity index 95%
rename from action/src/test/java/com/adyen/checkout/action/internal/ui/DefaultGenericActionDelegateTest.kt
rename to action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt
index ac1ff92baa..d82f24111b 100644
--- a/action/src/test/java/com/adyen/checkout/action/internal/ui/DefaultGenericActionDelegateTest.kt
+++ b/action-core/src/test/java/com/adyen/checkout/action/core/ui/DefaultGenericActionDelegateTest.kt
@@ -6,14 +6,18 @@
* Created by josephj on 20/9/2022.
*/
-package com.adyen.checkout.action.internal.ui
+package com.adyen.checkout.action.core.ui
import android.app.Activity
import android.app.Application
import android.content.Intent
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
-import com.adyen.checkout.action.GenericActionConfiguration
+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.action.RedirectAction
import com.adyen.checkout.components.core.action.Threeds2ChallengeAction
diff --git a/action/build.gradle b/action/build.gradle
index 05e6681da6..6f5e7bfc93 100644
--- a/action/build.gradle
+++ b/action/build.gradle
@@ -36,11 +36,8 @@ android {
dependencies {
// Checkout
api project(':3ds2')
- api project(':await')
- api project(':qr-code')
- api project(':redirect')
+ api project(':action-core')
api project(':wechatpay')
- api project(':voucher')
//Tests
testImplementation project(':test-core')
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 49e52264ae..c43dfee339 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
@@ -36,8 +36,9 @@ 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
+class AwaitComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class AwaitComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) : ActionComponentProvider {
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 c21961a31b..ee6e9c51c8 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
@@ -53,7 +53,7 @@ internal class AwaitView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is AwaitDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is AwaitDelegate) { "Unsupported delegate type" }
this.delegate = delegate
diff --git a/bacs/build.gradle b/bacs/build.gradle
index b329004ba2..d50e338122 100644
--- a/bacs/build.gradle
+++ b/bacs/build.gradle
@@ -40,7 +40,7 @@ android {
dependencies {
// Checkout
- api project(':action')
+ api project(':action-core')
api project(':ui-core')
api project(':sessions-core')
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 f902739469..e3928f6023 100644
--- a/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitComponent.kt
+++ b/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitComponent.kt
@@ -11,9 +11,9 @@ package com.adyen.checkout.bacs
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.adyen.checkout.action.internal.ActionHandlingComponent
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.ActionHandlingComponent
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.bacs.internal.provider.BacsDirectDebitComponentProvider
import com.adyen.checkout.bacs.internal.ui.BacsDirectDebitDelegate
import com.adyen.checkout.components.core.PaymentMethodTypes
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 0ce0efc91b..6cf7d5b250 100644
--- a/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitConfiguration.kt
+++ b/bacs/src/main/java/com/adyen/checkout/bacs/BacsDirectDebitConfiguration.kt
@@ -9,8 +9,8 @@
package com.adyen.checkout.bacs
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.ActionHandlingPaymentMethodConfigurationBuilder
+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.internal.ButtonConfiguration
import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
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 d137a04e88..bd26f45c18 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
@@ -14,8 +14,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider
import com.adyen.checkout.bacs.BacsDirectDebitComponent
import com.adyen.checkout.bacs.BacsDirectDebitComponentState
import com.adyen.checkout.bacs.BacsDirectDebitConfiguration
@@ -48,8 +48,9 @@ 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
+class BacsDirectDebitComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class BacsDirectDebitComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) :
@@ -57,12 +58,14 @@ class BacsDirectDebitComponentProvider(
BacsDirectDebitComponent,
BacsDirectDebitConfiguration,
BacsDirectDebitComponentState,
- ComponentCallback>,
+ ComponentCallback
+ >,
SessionPaymentComponentProvider<
BacsDirectDebitComponent,
BacsDirectDebitConfiguration,
BacsDirectDebitComponentState,
- SessionComponentCallback> {
+ SessionComponentCallback
+ > {
private val componentParamsMapper = ButtonComponentParamsMapper(overrideComponentParams, overrideSessionParams)
diff --git a/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitConfirmationView.kt b/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitConfirmationView.kt
index d8c947e753..952e451b12 100644
--- a/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitConfirmationView.kt
+++ b/bacs/src/main/java/com/adyen/checkout/bacs/internal/ui/view/BacsDirectDebitConfirmationView.kt
@@ -47,7 +47,7 @@ internal class BacsDirectDebitConfirmationView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is BacsDirectDebitDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is BacsDirectDebitDelegate) { "Unsupported delegate type" }
bacsDelegate = delegate
this.localizedContext = localizedContext
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 cfa32f96fd..6e7087bc5d 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
@@ -64,7 +64,7 @@ internal class BacsDirectDebitInputView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is BacsDirectDebitDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is BacsDirectDebitDelegate) { "Unsupported delegate type" }
bacsDelegate = delegate
this.localizedContext = localizedContext
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 152fe74404..b886ca5bca 100644
--- a/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitComponentTest.kt
+++ b/bacs/src/test/java/com/adyen/checkout/bacs/BacsDirectDebitComponentTest.kt
@@ -11,8 +11,8 @@ package com.adyen.checkout.bacs
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.bacs.internal.ui.BacsComponentViewType
import com.adyen.checkout.bacs.internal.ui.BacsDirectDebitDelegate
import com.adyen.checkout.components.core.internal.ComponentEventHandler
diff --git a/bcmc/build.gradle b/bcmc/build.gradle
index c86b2fe01d..75ae403ba8 100644
--- a/bcmc/build.gradle
+++ b/bcmc/build.gradle
@@ -44,9 +44,10 @@ android {
dependencies {
// Checkout
- api project(":action")
- api project(":card")
- api project(":sessions-core")
+ api project(':3ds2')
+ api project(':action-core')
+ api project(':card')
+ api project(':sessions-core')
// If 3DS2 SDK is present.
compileOnly libraries.adyen3ds2
diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt
index 8b605f0535..e784956fdf 100644
--- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt
+++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcComponent.kt
@@ -10,9 +10,9 @@ package com.adyen.checkout.bcmc
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.adyen.checkout.action.internal.ActionHandlingComponent
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.ActionHandlingComponent
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.bcmc.internal.provider.BcmcComponentProvider
import com.adyen.checkout.bcmc.internal.ui.BcmcDelegate
import com.adyen.checkout.card.CardBrand
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 cbaf613673..92261f3547 100644
--- a/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.kt
+++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/BcmcConfiguration.kt
@@ -8,9 +8,10 @@
package com.adyen.checkout.bcmc
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.ActionHandlingPaymentMethodConfigurationBuilder
+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.PaymentComponentData
import com.adyen.checkout.components.core.internal.ButtonConfiguration
import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
import com.adyen.checkout.components.core.internal.Configuration
@@ -105,8 +106,7 @@ class BcmcConfiguration private constructor(
/**
* Set the unique reference for the shopper doing this transaction.
- * This value will simply be passed back to you in the
- * [com.adyen.checkout.components.model.payments.request.PaymentComponentData] for convenience.
+ * This value will simply be passed back to you in the [PaymentComponentData] for convenience.
*
* @param shopperReference The unique shopper reference
* @return [BcmcConfiguration.Builder]
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 8886b44532..9bce5e6d53 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
@@ -14,8 +14,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider
import com.adyen.checkout.bcmc.BcmcComponent
import com.adyen.checkout.bcmc.BcmcComponentState
import com.adyen.checkout.bcmc.BcmcConfiguration
@@ -55,8 +55,9 @@ 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
+class BcmcComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class BcmcComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) :
@@ -64,12 +65,14 @@ class BcmcComponentProvider(
BcmcComponent,
BcmcConfiguration,
BcmcComponentState,
- ComponentCallback>,
+ ComponentCallback
+ >,
SessionPaymentComponentProvider<
BcmcComponent,
BcmcConfiguration,
BcmcComponentState,
- SessionComponentCallback> {
+ SessionComponentCallback
+ > {
private val componentParamsMapper = BcmcComponentParamsMapper(overrideComponentParams, overrideSessionParams)
diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt
index ad4bd1fdf5..75be64827a 100644
--- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt
+++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegate.kt
@@ -38,6 +38,7 @@ 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.runCompileOnly
import com.adyen.checkout.cse.EncryptedCard
import com.adyen.checkout.cse.EncryptionException
import com.adyen.checkout.cse.UnencryptedCard
@@ -223,7 +224,8 @@ internal class DefaultBcmcDelegate(
encryptedCardNumber = encryptedCard.encryptedCardNumber,
encryptedExpiryMonth = encryptedCard.encryptedExpiryMonth,
encryptedExpiryYear = encryptedCard.encryptedExpiryYear,
- threeDS2SdkVersion = get3DS2SdkVersion(),
+ threeDS2SdkVersion = runCompileOnly { ThreeDS2Service.INSTANCE.sdkVersion },
+ brand = PaymentMethodTypes.BCMC
).apply {
if (componentParams.isHolderNameRequired) {
holderName = outputData.cardHolderNameField.value
@@ -267,16 +269,6 @@ internal class DefaultBcmcDelegate(
null
}
- private fun get3DS2SdkVersion(): String? = try {
- ThreeDS2Service.INSTANCE.sdkVersion
- } catch (e: ClassNotFoundException) {
- Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.")
- null
- } catch (e: NoClassDefFoundError) {
- Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.")
- null
- }
-
override fun getPaymentMethodType(): String {
return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN
}
diff --git a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt
index 32a86ed5c0..160659964c 100644
--- a/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt
+++ b/bcmc/src/main/java/com/adyen/checkout/bcmc/internal/ui/view/BcmcView.kt
@@ -54,7 +54,7 @@ internal class BcmcView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is BcmcDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is BcmcDelegate) { "Unsupported delegate type" }
this.delegate = delegate
this.localizedContext = localizedContext
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 b354602967..92cacce9c3 100644
--- a/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt
+++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/BcmcComponentTest.kt
@@ -11,8 +11,8 @@ package com.adyen.checkout.bcmc
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.bcmc.internal.ui.BcmcComponentViewType
import com.adyen.checkout.bcmc.internal.ui.BcmcDelegate
import com.adyen.checkout.components.core.internal.ComponentEventHandler
diff --git a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt
index 12cd54f5d9..24e2b2f0f8 100644
--- a/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt
+++ b/bcmc/src/test/java/com/adyen/checkout/bcmc/internal/ui/DefaultBcmcDelegateTest.kt
@@ -19,6 +19,7 @@ import com.adyen.checkout.card.internal.ui.model.ExpiryDate
import com.adyen.checkout.components.core.Amount
import com.adyen.checkout.components.core.OrderRequest
import com.adyen.checkout.components.core.PaymentMethod
+import com.adyen.checkout.components.core.PaymentMethodTypes
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
@@ -288,6 +289,7 @@ internal class DefaultBcmcDelegateTest(
assertTrue(isValid)
assertTrue(isInputValid)
assertEquals(TEST_ORDER, data.order)
+ assertEquals(PaymentMethodTypes.BCMC, data.paymentMethod?.brand)
}
}
}
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 f46bc53171..89d9e433df 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
@@ -110,7 +110,8 @@ internal class BcmcComponentParamsMapperTest {
sessionParams = SessionParams(
enableStoreDetails = sessionsValue,
installmentOptions = null,
- amount = null
+ amount = null,
+ returnUrl = "",
)
)
@@ -140,7 +141,8 @@ internal class BcmcComponentParamsMapperTest {
sessionParams = SessionParams(
enableStoreDetails = null,
installmentOptions = null,
- amount = sessionsValue
+ amount = sessionsValue,
+ returnUrl = "",
)
)
diff --git a/blik/build.gradle b/blik/build.gradle
index f7c811b355..96636feda0 100644
--- a/blik/build.gradle
+++ b/blik/build.gradle
@@ -40,7 +40,7 @@ android {
dependencies {
// Checkout
- api project(':action')
+ api project(':action-core')
api project(':ui-core')
api project(':sessions-core')
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 ce075d377d..d0523b0d15 100644
--- a/blik/src/main/java/com/adyen/checkout/blik/BlikComponent.kt
+++ b/blik/src/main/java/com/adyen/checkout/blik/BlikComponent.kt
@@ -10,9 +10,9 @@ package com.adyen.checkout.blik
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.adyen.checkout.action.internal.ActionHandlingComponent
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.ActionHandlingComponent
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.blik.internal.provider.BlikComponentProvider
import com.adyen.checkout.blik.internal.ui.BlikDelegate
import com.adyen.checkout.components.core.PaymentMethodTypes
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 d999d5ac13..2c3846613b 100644
--- a/blik/src/main/java/com/adyen/checkout/blik/BlikConfiguration.kt
+++ b/blik/src/main/java/com/adyen/checkout/blik/BlikConfiguration.kt
@@ -8,8 +8,8 @@
package com.adyen.checkout.blik
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.ActionHandlingPaymentMethodConfigurationBuilder
+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.internal.ButtonConfiguration
import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
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 cf32e780d6..47977e817e 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
@@ -14,8 +14,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider
import com.adyen.checkout.blik.BlikComponent
import com.adyen.checkout.blik.BlikComponentState
import com.adyen.checkout.blik.BlikConfiguration
@@ -52,8 +52,9 @@ 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
+class BlikComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class BlikComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) :
@@ -61,22 +62,26 @@ class BlikComponentProvider(
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)
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 992c37f4d6..0691f4c6e0 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
@@ -52,7 +52,7 @@ internal class BlikView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is BlikDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is BlikDelegate) { "Unsupported delegate type" }
blikDelegate = delegate
this.localizedContext = localizedContext
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 8f4335990a..a83887ea0b 100644
--- a/blik/src/test/java/com/adyen/checkout/blik/BlikComponentTest.kt
+++ b/blik/src/test/java/com/adyen/checkout/blik/BlikComponentTest.kt
@@ -11,8 +11,8 @@ package com.adyen.checkout.blik
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.blik.internal.ui.BlikComponentViewType
import com.adyen.checkout.blik.internal.ui.BlikDelegate
import com.adyen.checkout.components.core.internal.ComponentEventHandler
diff --git a/boleto/.gitignore b/boleto/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/boleto/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/boleto/build.gradle b/boleto/build.gradle
new file mode 100644
index 0000000000..26297d34c5
--- /dev/null
+++ b/boleto/build.gradle
@@ -0,0 +1,54 @@
+/*
+ * 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 atef on 3/3/2023.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+ id 'kotlin-parcelize'
+}
+
+ext.mavenArtifactId = "boleto"
+ext.mavenArtifactName = "Adyen checkout Boleto component"
+ext.mavenArtifactDescription = "Adyen checkout Boleto component client for Adyen's Checkout API."
+
+apply from: "${rootDir}/config/gradle/sharedTasks.gradle"
+
+android {
+ namespace 'com.adyen.checkout.boleto'
+ compileSdkVersion compile_sdk_version
+
+ defaultConfig {
+ minSdkVersion min_sdk_version
+ targetSdkVersion target_sdk_version
+ versionCode version_code
+ versionName version_name
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ // Checkout
+ api project(':action-core')
+ api project(':ui-core')
+ api project(':sessions-core')
+
+ // Dependencies
+ implementation libraries.material
+
+ //Tests
+ testImplementation project(':test-core')
+ testImplementation testLibraries.junit5
+ testImplementation testLibraries.kotlinCoroutines
+ testImplementation testLibraries.mockito
+}
diff --git a/boleto/consumer-rules.pro b/boleto/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/boleto/proguard-rules.pro b/boleto/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/boleto/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/boleto/src/main/AndroidManifest.xml b/boleto/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..722102298a
--- /dev/null
+++ b/boleto/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt
new file mode 100644
index 0000000000..fc52a77979
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponent.kt
@@ -0,0 +1,115 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto
+
+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.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
+import com.adyen.checkout.boleto.internal.provider.BoletoComponentProvider
+import com.adyen.checkout.boleto.internal.ui.BoletoDelegate
+import com.adyen.checkout.components.core.PaymentMethodTypes
+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.ui.core.internal.ui.ButtonDelegate
+import com.adyen.checkout.ui.core.internal.ui.ComponentViewType
+import com.adyen.checkout.ui.core.internal.ui.ViewableComponent
+import com.adyen.checkout.ui.core.internal.util.mergeViewFlows
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A [PaymentComponent] that supports the [PaymentMethodTypes.BOLETOBANCARIO],
+ * [PaymentMethodTypes.BOLETOBANCARIO_BANCODOBRASIL], [PaymentMethodTypes.BOLETOBANCARIO_BRADESCO],
+ * [PaymentMethodTypes.BOLETOBANCARIO_HSBC], [PaymentMethodTypes.BOLETOBANCARIO_ITAU],
+ * [PaymentMethodTypes.BOLETOBANCARIO_SANTANDER] and [PaymentMethodTypes.BOLETO_PRIMEIRO_PAY] payment methods.
+ */
+class BoletoComponent internal constructor(
+ private val boletoDelegate: BoletoDelegate,
+ private val genericActionDelegate: GenericActionDelegate,
+ private val actionHandlingComponent: DefaultActionHandlingComponent,
+ internal val componentEventHandler: ComponentEventHandler,
+) : ViewModel(),
+ PaymentComponent,
+ ViewableComponent,
+ ButtonComponent,
+ ActionHandlingComponent by actionHandlingComponent {
+
+ override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate
+
+ override val viewFlow: Flow = mergeViewFlows(
+ viewModelScope,
+ boletoDelegate.viewFlow,
+ genericActionDelegate.viewFlow,
+ )
+
+ init {
+ boletoDelegate.initialize(viewModelScope)
+ genericActionDelegate.initialize(viewModelScope)
+ componentEventHandler.initialize(viewModelScope)
+ }
+
+ internal fun observe(
+ lifecycleOwner: LifecycleOwner,
+ callback: (PaymentComponentEvent) -> Unit
+ ) {
+ boletoDelegate.observe(lifecycleOwner, viewModelScope, callback)
+ genericActionDelegate.observe(lifecycleOwner, viewModelScope, callback.toActionCallback())
+ }
+
+ internal fun removeObserver() {
+ boletoDelegate.removeObserver()
+ genericActionDelegate.removeObserver()
+ }
+
+ override fun isConfirmationRequired(): Boolean = boletoDelegate.isConfirmationRequired()
+
+ override fun submit() {
+ (delegate as? ButtonDelegate)?.onSubmit()
+ ?: Logger.e(TAG, "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.")
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ Logger.d(TAG, "onCleared")
+ boletoDelegate.onCleared()
+ genericActionDelegate.onCleared()
+ componentEventHandler.onCleared()
+ }
+
+ companion object {
+ private val TAG = LogUtil.getTag()
+
+ @JvmField
+ val PROVIDER = BoletoComponentProvider()
+
+ @JvmField
+ val PAYMENT_METHOD_TYPES = listOf(
+ PaymentMethodTypes.BOLETOBANCARIO,
+ PaymentMethodTypes.BOLETOBANCARIO_BANCODOBRASIL,
+ PaymentMethodTypes.BOLETOBANCARIO_BRADESCO,
+ PaymentMethodTypes.BOLETOBANCARIO_HSBC,
+ PaymentMethodTypes.BOLETOBANCARIO_ITAU,
+ PaymentMethodTypes.BOLETOBANCARIO_SANTANDER,
+ PaymentMethodTypes.BOLETO_PRIMEIRO_PAY,
+ )
+ }
+}
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponentState.kt b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponentState.kt
new file mode 100644
index 0000000000..c248ee4143
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoComponentState.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto
+
+import com.adyen.checkout.components.core.PaymentComponentData
+import com.adyen.checkout.components.core.PaymentComponentState
+import com.adyen.checkout.components.core.paymentmethod.GenericPaymentMethod
+
+/**
+ * Represents the state of [BoletoComponent].
+ */
+data class BoletoComponentState(
+ override val data: PaymentComponentData,
+ override val isInputValid: Boolean,
+ override val isReady: Boolean,
+) : PaymentComponentState
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt
new file mode 100644
index 0000000000..8da040f6cd
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/BoletoConfiguration.kt
@@ -0,0 +1,107 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto
+
+import android.content.Context
+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.internal.ButtonConfiguration
+import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
+import com.adyen.checkout.components.core.internal.Configuration
+import com.adyen.checkout.core.Environment
+import kotlinx.parcelize.Parcelize
+import java.util.Locale
+
+/**
+ * Configuration class for the [BoletoComponent].
+ */
+@Parcelize
+@Suppress("LongParameterList")
+class BoletoConfiguration private constructor(
+ override val shopperLocale: Locale,
+ override val environment: Environment,
+ override val clientKey: String,
+ override val isAnalyticsEnabled: Boolean?,
+ override val amount: Amount,
+ override val isSubmitButtonVisible: Boolean?,
+ val genericActionConfiguration: GenericActionConfiguration,
+ val isEmailVisible: Boolean?
+) : Configuration, ButtonConfiguration {
+
+ /**
+ * Builder to create a [BoletoConfiguration].
+ */
+ class Builder :
+ ActionHandlingPaymentMethodConfigurationBuilder,
+ ButtonConfigurationBuilder {
+ private var isSubmitButtonVisible: Boolean? = null
+ private var isEmailVisible: Boolean? = null
+
+ /**
+ * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale.
+ *
+ * @param context A Context
+ * @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(context: Context, environment: Environment, clientKey: String) : super(
+ context,
+ environment,
+ clientKey
+ )
+
+ /**
+ * Builder with parameters for a [BoletoConfiguration].
+ *
+ * @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, environment: Environment, clientKey: String) : super(
+ shopperLocale,
+ environment,
+ clientKey
+ )
+
+ /**
+ * Sets if submit button will be visible or not.
+ *
+ * Default is True.
+ *
+ * @param isSubmitButtonVisible Is submit button should be visible or not.
+ */
+ override fun setSubmitButtonVisible(isSubmitButtonVisible: Boolean): Builder {
+ this.isSubmitButtonVisible = isSubmitButtonVisible
+ return this
+ }
+
+ /**
+ * Sets the visibility of the "send email copy"-switch and email input field.
+ *
+ * Default value is false
+ * @param isEmailVisible
+ */
+ fun setEmailVisibility(isEmailVisible: Boolean): Builder {
+ this.isEmailVisible = isEmailVisible
+ return this
+ }
+
+ override fun buildInternal() = BoletoConfiguration(
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey,
+ isAnalyticsEnabled = isAnalyticsEnabled,
+ amount = amount,
+ isSubmitButtonVisible = isSubmitButtonVisible,
+ genericActionConfiguration = genericActionConfigurationBuilder.build(),
+ isEmailVisible = isEmailVisible
+ )
+ }
+}
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
new file mode 100644
index 0000000000..ddf9bfbb56
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/provider/BoletoComponentProvider.kt
@@ -0,0 +1,226 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.provider
+
+import android.app.Application
+import androidx.annotation.RestrictTo
+import androidx.lifecycle.LifecycleOwner
+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.boleto.BoletoComponent
+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.components.core.ComponentCallback
+import com.adyen.checkout.components.core.Order
+import com.adyen.checkout.components.core.PaymentMethod
+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
+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.data.model.AnalyticsSource
+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.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.sessions.core.CheckoutSession
+import com.adyen.checkout.sessions.core.SessionComponentCallback
+import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler
+import com.adyen.checkout.sessions.core.internal.SessionInteractor
+import com.adyen.checkout.sessions.core.internal.SessionSavedStateHandleContainer
+import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository
+import com.adyen.checkout.sessions.core.internal.data.api.SessionService
+import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponentProvider
+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.SubmitHandler
+
+class BoletoComponentProvider
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(
+ overrideComponentParams: ComponentParams? = null,
+ overrideSessionParams: SessionParams? = null,
+) :
+ PaymentComponentProvider<
+ BoletoComponent,
+ BoletoConfiguration,
+ BoletoComponentState,
+ ComponentCallback
+ >,
+ SessionPaymentComponentProvider<
+ BoletoComponent,
+ BoletoConfiguration,
+ BoletoComponentState,
+ SessionComponentCallback
+ > {
+
+ private val componentParamsMapper = BoletoComponentParamsMapper(overrideComponentParams, overrideSessionParams)
+
+ override fun get(
+ savedStateRegistryOwner: SavedStateRegistryOwner,
+ viewModelStoreOwner: ViewModelStoreOwner,
+ lifecycleOwner: LifecycleOwner,
+ paymentMethod: PaymentMethod,
+ configuration: BoletoConfiguration,
+ application: Application,
+ componentCallback: ComponentCallback,
+ order: Order?,
+ key: String?
+ ): BoletoComponent {
+ assertSupported(paymentMethod)
+
+ val boletoFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle ->
+ val componentParams = componentParamsMapper.mapToParams(configuration, null)
+ val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
+ val analyticsService = AnalyticsService(httpClient)
+ val analyticsRepository = DefaultAnalyticsRepository(
+ packageName = application.packageName,
+ locale = componentParams.shopperLocale,
+ source = AnalyticsSource.PaymentComponent(componentParams.isCreatedByDropIn, paymentMethod),
+ analyticsService = analyticsService,
+ analyticsMapper = AnalyticsMapper(),
+ )
+
+ val addressService = AddressService(httpClient)
+ val addressRepository = DefaultAddressRepository(addressService)
+
+ val boletoDelegate = DefaultBoletoDelegate(
+ submitHandler = SubmitHandler(savedStateHandle),
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = paymentMethod,
+ order = order,
+ componentParams = componentParams,
+ addressRepository = addressRepository
+ )
+
+ val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate(
+ configuration = configuration.genericActionConfiguration,
+ savedStateHandle = savedStateHandle,
+ application = application,
+ )
+
+ BoletoComponent(
+ boletoDelegate = boletoDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, boletoDelegate),
+ componentEventHandler = DefaultComponentEventHandler(),
+ )
+ }
+
+ return ViewModelProvider(viewModelStoreOwner, boletoFactory)[key, BoletoComponent::class.java]
+ .also { component ->
+ component.observe(lifecycleOwner) {
+ component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
+ }
+ }
+ }
+
+ @Suppress("LongMethod")
+ override fun get(
+ savedStateRegistryOwner: SavedStateRegistryOwner,
+ viewModelStoreOwner: ViewModelStoreOwner,
+ lifecycleOwner: LifecycleOwner,
+ checkoutSession: CheckoutSession,
+ paymentMethod: PaymentMethod,
+ configuration: BoletoConfiguration,
+ application: Application,
+ componentCallback: SessionComponentCallback,
+ key: String?
+ ): BoletoComponent {
+ assertSupported(paymentMethod)
+
+ val genericFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle ->
+ val componentParams = componentParamsMapper.mapToParams(
+ configuration = configuration,
+ sessionParams = SessionParamsFactory.create(checkoutSession),
+ )
+ val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
+ val analyticsService = AnalyticsService(httpClient)
+ val analyticsRepository = DefaultAnalyticsRepository(
+ packageName = application.packageName,
+ locale = componentParams.shopperLocale,
+ source = AnalyticsSource.PaymentComponent(componentParams.isCreatedByDropIn, paymentMethod),
+ analyticsService = analyticsService,
+ analyticsMapper = AnalyticsMapper(),
+ )
+ val addressService = AddressService(httpClient)
+ val addressRepository = DefaultAddressRepository(addressService)
+
+ val boletoDelegate = DefaultBoletoDelegate(
+ submitHandler = SubmitHandler(savedStateHandle),
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = paymentMethod,
+ order = checkoutSession.order,
+ componentParams = componentParams,
+ addressRepository = addressRepository
+ )
+
+ val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate(
+ configuration = configuration.genericActionConfiguration,
+ savedStateHandle = savedStateHandle,
+ application = application,
+ )
+
+ 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
+ )
+
+ val sessionComponentEventHandler =
+ SessionComponentEventHandler(
+ sessionInteractor = sessionInteractor,
+ sessionSavedStateHandleContainer = sessionSavedStateHandleContainer,
+ )
+
+ BoletoComponent(
+ boletoDelegate = boletoDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, boletoDelegate),
+ componentEventHandler = sessionComponentEventHandler,
+ )
+ }
+
+ return ViewModelProvider(viewModelStoreOwner, genericFactory)[key, BoletoComponent::class.java]
+ .also { component ->
+ component.observe(lifecycleOwner) {
+ component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
+ }
+ }
+ }
+
+ private fun assertSupported(paymentMethod: PaymentMethod) {
+ if (!isPaymentMethodSupported(paymentMethod)) {
+ throw ComponentException("Unsupported payment method ${paymentMethod.type}")
+ }
+ }
+
+ override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean {
+ return BoletoComponent.PAYMENT_METHOD_TYPES.contains(paymentMethod.type)
+ }
+}
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/BoletoDelegate.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/BoletoDelegate.kt
new file mode 100644
index 0000000000..84b1401008
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/BoletoDelegate.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui
+
+import com.adyen.checkout.boleto.BoletoComponentState
+import com.adyen.checkout.boleto.internal.ui.model.BoletoInputData
+import com.adyen.checkout.boleto.internal.ui.model.BoletoOutputData
+import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate
+import com.adyen.checkout.ui.core.internal.ui.AddressDelegate
+import com.adyen.checkout.ui.core.internal.ui.ButtonDelegate
+import com.adyen.checkout.ui.core.internal.ui.UIStateDelegate
+import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate
+import kotlinx.coroutines.flow.Flow
+
+internal interface BoletoDelegate :
+ PaymentComponentDelegate,
+ ViewProvidingDelegate,
+ ButtonDelegate,
+ UIStateDelegate,
+ AddressDelegate {
+
+ val outputData: BoletoOutputData
+
+ val outputDataFlow: Flow
+
+ val componentStateFlow: Flow
+
+ fun updateInputData(update: BoletoInputData.() -> Unit)
+
+ fun setInteractionBlocked(isInteractionBlocked: Boolean)
+}
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/BoletoViewProvider.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/BoletoViewProvider.kt
new file mode 100644
index 0000000000..5bad258611
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/BoletoViewProvider.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import com.adyen.checkout.boleto.R
+import com.adyen.checkout.boleto.internal.ui.view.BoletoView
+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
+
+internal object BoletoViewProvider : ViewProvider {
+ override fun getView(
+ viewType: ComponentViewType,
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ): ComponentView = when (viewType) {
+ BoletoComponentViewType -> BoletoView(context, attrs, defStyleAttr)
+ else -> throw IllegalArgumentException("Unsupported view type")
+ }
+}
+
+internal object BoletoComponentViewType : ButtonComponentViewType {
+
+ override val viewProvider: ViewProvider = BoletoViewProvider
+
+ override val buttonTextResId: Int = R.string.checkout_boleto_generate_btn_label
+}
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
new file mode 100644
index 0000000000..e063c48304
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegate.kt
@@ -0,0 +1,306 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui
+
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import com.adyen.checkout.boleto.BoletoComponentState
+import com.adyen.checkout.boleto.internal.ui.model.BoletoComponentParams
+import com.adyen.checkout.boleto.internal.ui.model.BoletoInputData
+import com.adyen.checkout.boleto.internal.ui.model.BoletoOutputData
+import com.adyen.checkout.boleto.internal.util.BoletoValidationUtils
+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.PaymentMethodTypes
+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.util.isEmpty
+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.ui.core.internal.data.api.AddressRepository
+import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState
+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
+import com.adyen.checkout.ui.core.internal.util.AddressFormUtils
+import com.adyen.checkout.ui.core.internal.util.AddressValidationUtils
+import com.adyen.checkout.ui.core.internal.util.SocialSecurityNumberUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+@Suppress("TooManyFunctions", "LongParameterList")
+internal class DefaultBoletoDelegate(
+ private val submitHandler: SubmitHandler,
+ private val analyticsRepository: AnalyticsRepository,
+ private val observerRepository: PaymentObserverRepository,
+ private val paymentMethod: PaymentMethod,
+ private val order: OrderRequest?,
+ override val componentParams: BoletoComponentParams,
+ private val addressRepository: AddressRepository,
+) : BoletoDelegate {
+ private val inputData = BoletoInputData()
+
+ override val outputData: BoletoOutputData get() = _outputDataFlow.value
+
+ private val _outputDataFlow = MutableStateFlow(createOutputData())
+ override val outputDataFlow: Flow = _outputDataFlow
+
+ override val addressOutputData: AddressOutputData
+ get() = outputData.addressState
+
+ override val addressOutputDataFlow: Flow by lazy {
+ outputDataFlow.map {
+ it.addressState
+ }.stateIn(coroutineScope, SharingStarted.Lazily, outputData.addressState)
+ }
+
+ private val _componentStateFlow = MutableStateFlow(createComponentState())
+ override val componentStateFlow: Flow = _componentStateFlow
+
+ private val _viewFlow: MutableStateFlow = MutableStateFlow(BoletoComponentViewType)
+ override val viewFlow: Flow = _viewFlow
+
+ override val submitFlow: Flow = submitHandler.submitFlow
+ override val uiStateFlow: Flow = submitHandler.uiStateFlow
+ override val uiEventFlow: Flow = submitHandler.uiEventFlow
+
+ private var _coroutineScope: CoroutineScope? = null
+ private val coroutineScope: CoroutineScope get() = requireNotNull(_coroutineScope)
+
+ override fun initialize(coroutineScope: CoroutineScope) {
+ _coroutineScope = coroutineScope
+ submitHandler.initialize(coroutineScope, componentStateFlow)
+
+ sendAnalyticsEvent(coroutineScope)
+
+ if (componentParams.addressParams is AddressParams.FullAddress) {
+ subscribeToStatesList()
+ subscribeToCountryList()
+ requestCountryList()
+ }
+ }
+
+ private fun sendAnalyticsEvent(coroutineScope: CoroutineScope) {
+ Logger.v(TAG, "sendAnalyticsEvent")
+ coroutineScope.launch {
+ analyticsRepository.sendAnalyticsEvent()
+ }
+ }
+
+ private fun subscribeToStatesList() {
+ addressRepository.statesFlow
+ .distinctUntilChanged()
+ .onEach { states ->
+ Logger.d(TAG, "New states emitted - states: ${states.size}")
+ updateOutputData(stateOptions = AddressFormUtils.initializeStateOptions(states))
+ }
+ .launchIn(coroutineScope)
+ }
+
+ private fun subscribeToCountryList() {
+ addressRepository.countriesFlow
+ .distinctUntilChanged()
+ .onEach { countries ->
+ val countryOptions = AddressFormUtils.initializeCountryOptions(
+ shopperLocale = componentParams.shopperLocale,
+ addressParams = componentParams.addressParams,
+ countryList = countries
+ )
+ countryOptions.firstOrNull { it.selected }?.let {
+ inputData.address.country = it.code
+ requestStateList(it.code)
+ }
+ updateOutputData(countryOptions = countryOptions)
+ }
+ .launchIn(coroutineScope)
+ }
+
+ private fun requestCountryList() {
+ addressRepository.getCountryList(
+ shopperLocale = componentParams.shopperLocale,
+ coroutineScope = coroutineScope
+ )
+ }
+
+ private fun requestStateList(countryCode: String?) {
+ addressRepository.getStateList(
+ shopperLocale = componentParams.shopperLocale,
+ countryCode = countryCode,
+ coroutineScope = coroutineScope
+ )
+ }
+
+ override fun updateAddressInputData(update: AddressInputModel.() -> Unit) {
+ updateInputData {
+ this.address.update()
+ }
+ }
+
+ override fun updateInputData(update: BoletoInputData.() -> Unit) {
+ inputData.update()
+ onInputDataChanged()
+ }
+
+ private fun onInputDataChanged() {
+ val outputData = createOutputData(
+ countryOptions = outputData.addressState.countryOptions,
+ stateOptions = outputData.addressState.stateOptions
+ )
+ _outputDataFlow.tryEmit(outputData)
+ updateComponentState(outputData)
+ requestStateList(inputData.address.country)
+ }
+
+ private fun updateOutputData(
+ countryOptions: List = outputData.addressState.countryOptions,
+ stateOptions: List = outputData.addressState.stateOptions,
+ ) {
+ val newOutputData = createOutputData(countryOptions, stateOptions)
+ _outputDataFlow.tryEmit(newOutputData)
+ updateComponentState(newOutputData)
+ }
+
+ private fun createOutputData(
+ countryOptions: List = emptyList(),
+ stateOptions: List = emptyList(),
+ ): BoletoOutputData {
+ val updatedCountryOptions = AddressFormUtils.markAddressListItemSelected(
+ countryOptions,
+ inputData.address.country
+ )
+ val updatedStateOptions = AddressFormUtils.markAddressListItemSelected(
+ stateOptions,
+ inputData.address.stateOrProvince
+ )
+
+ val addressFormUIState = AddressFormUIState.fromAddressParams(componentParams.addressParams)
+
+ return BoletoOutputData(
+ firstNameState = BoletoValidationUtils.validateFirstName(inputData.firstName),
+ lastNameState = BoletoValidationUtils.validateLastName(inputData.lastName),
+ socialSecurityNumberState = SocialSecurityNumberUtils.validateSocialSecurityNumber(
+ inputData.socialSecurityNumber
+ ),
+ addressState = AddressValidationUtils.validateAddressInput(
+ inputData.address,
+ addressFormUIState,
+ updatedCountryOptions,
+ updatedStateOptions,
+ false
+ ),
+ addressUIState = addressFormUIState,
+ isEmailVisible = componentParams.isEmailVisible,
+ isSendEmailSelected = inputData.isSendEmailSelected,
+ shopperEmailState = BoletoValidationUtils.validateShopperEmail(
+ inputData.isSendEmailSelected,
+ inputData.shopperEmail
+ )
+ )
+ }
+
+ @VisibleForTesting
+ internal fun updateComponentState(outputData: BoletoOutputData) {
+ Logger.v(TAG, "updateComponentState")
+ val componentState = createComponentState(outputData)
+ _componentStateFlow.tryEmit(componentState)
+ }
+
+ private fun createComponentState(
+ outputData: BoletoOutputData = this.outputData
+ ): BoletoComponentState {
+ val paymentComponentData = PaymentComponentData(
+ paymentMethod = GenericPaymentMethod(paymentMethod.type),
+ order = order,
+ amount = componentParams.amount.takeUnless { it.isEmpty },
+ socialSecurityNumber = outputData.socialSecurityNumberState.value,
+ shopperName = ShopperName(
+ firstName = outputData.firstNameState.value,
+ lastName = outputData.lastNameState.value
+ )
+ )
+ if (outputData.isSendEmailSelected) {
+ paymentComponentData.shopperEmail = outputData.shopperEmailState.value
+ }
+ if (AddressFormUtils.isAddressRequired(outputData.addressUIState)) {
+ paymentComponentData.billingAddress = AddressFormUtils.makeAddressData(
+ addressOutputData = outputData.addressState,
+ addressFormUIState = outputData.addressUIState
+ )
+ }
+ val countriesList: List = outputData.addressState.countryOptions
+ val statesList: List = outputData.addressState.stateOptions
+
+ return BoletoComponentState(
+ data = paymentComponentData,
+ isInputValid = outputData.isValid,
+ isReady = countriesList.isNotEmpty() && statesList.isNotEmpty()
+ )
+ }
+
+ override fun setInteractionBlocked(isInteractionBlocked: Boolean) {
+ submitHandler.setInteractionBlocked(isInteractionBlocked)
+ }
+
+ override fun getPaymentMethodType(): String {
+ return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN
+ }
+
+ override fun observe(
+ lifecycleOwner: LifecycleOwner,
+ coroutineScope: CoroutineScope,
+ callback: (PaymentComponentEvent) -> Unit
+ ) {
+ observerRepository.addObservers(
+ stateFlow = componentStateFlow,
+ exceptionFlow = null,
+ submitFlow = submitFlow,
+ lifecycleOwner = lifecycleOwner,
+ coroutineScope = coroutineScope,
+ callback = callback
+ )
+ }
+
+ override fun removeObserver() {
+ observerRepository.removeObservers()
+ }
+
+ override fun onSubmit() {
+ submitHandler.onSubmit(_componentStateFlow.value)
+ }
+
+ override fun isConfirmationRequired(): Boolean = _viewFlow.value is ButtonComponentViewType
+
+ override fun shouldShowSubmitButton(): Boolean = isConfirmationRequired() && componentParams.isSubmitButtonVisible
+
+ 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/AddressFieldPolicyParams.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/AddressFieldPolicyParams.kt
new file mode 100644
index 0000000000..964c6e6af1
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/AddressFieldPolicyParams.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.model
+
+import com.adyen.checkout.ui.core.internal.ui.model.AddressFieldPolicy
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+internal sealed class AddressFieldPolicyParams : AddressFieldPolicy {
+
+ /**
+ * Address form fields will be required.
+ */
+ @Parcelize
+ object Required : AddressFieldPolicyParams()
+}
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
new file mode 100644
index 0000000000..166cfb2cea
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParams.kt
@@ -0,0 +1,30 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.model
+
+import com.adyen.checkout.components.core.Amount
+import com.adyen.checkout.components.core.internal.ui.model.ButtonParams
+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 kotlinx.parcelize.Parcelize
+import java.util.Locale
+
+@Parcelize
+internal data class BoletoComponentParams(
+ override val isSubmitButtonVisible: Boolean,
+ override val shopperLocale: Locale,
+ override val environment: Environment,
+ override val clientKey: String,
+ override val isAnalyticsEnabled: Boolean,
+ override val isCreatedByDropIn: Boolean,
+ override val amount: Amount,
+ val addressParams: AddressParams,
+ val isEmailVisible: Boolean,
+) : ComponentParams, 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
new file mode 100644
index 0000000000..b4c267994e
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapper.kt
@@ -0,0 +1,78 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.model
+
+import com.adyen.checkout.boleto.BoletoConfiguration
+import com.adyen.checkout.components.core.internal.ui.model.ComponentParams
+import com.adyen.checkout.components.core.internal.ui.model.SessionParams
+import com.adyen.checkout.ui.core.internal.ui.model.AddressParams
+
+internal class BoletoComponentParamsMapper(
+ private val overrideComponentParams: ComponentParams?,
+ private val overrideSessionParams: SessionParams?,
+) {
+
+ fun mapToParams(
+ configuration: BoletoConfiguration,
+ sessionParams: SessionParams?
+ ): BoletoComponentParams {
+ return configuration
+ .mapToParamsInternal()
+ .override(overrideComponentParams)
+ .override(sessionParams ?: overrideSessionParams)
+ }
+
+ private fun BoletoConfiguration.mapToParamsInternal(): BoletoComponentParams {
+ return BoletoComponentParams(
+ isSubmitButtonVisible = isSubmitButtonVisible ?: true,
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey,
+ isAnalyticsEnabled = isAnalyticsEnabled ?: true,
+ isCreatedByDropIn = false,
+ amount = amount,
+ addressParams = AddressParams.FullAddress(
+ defaultCountryCode = BRAZIL_COUNTRY_CODE,
+ supportedCountryCodes = DEFAULT_SUPPORTED_COUNTRY_LIST,
+ 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,
+ isAnalyticsEnabled = overrideComponentParams.isAnalyticsEnabled,
+ isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn,
+ amount = overrideComponentParams.amount,
+ )
+ }
+
+ private fun BoletoComponentParams.override(
+ sessionParams: SessionParams?
+ ): BoletoComponentParams {
+ if (sessionParams == null) return this
+ return copy(
+ amount = sessionParams.amount ?: amount,
+ )
+ }
+
+ companion object {
+ private const val BRAZIL_COUNTRY_CODE = "BR"
+
+ // this payment method only works for Brazil so we don't need other countries inside country drop down
+ private val DEFAULT_SUPPORTED_COUNTRY_LIST = listOf(BRAZIL_COUNTRY_CODE)
+ }
+}
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
new file mode 100644
index 0000000000..30c5c82260
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoInputData.kt
@@ -0,0 +1,21 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.model
+
+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 = "",
+ var lastName: String = "",
+ var socialSecurityNumber: String = "",
+ var address: AddressInputModel = AddressInputModel(),
+ var isSendEmailSelected: Boolean = false,
+ var shopperEmail: String = ""
+) : InputData
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoOutputData.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoOutputData.kt
new file mode 100644
index 0000000000..f3ef7d9f75
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/model/BoletoOutputData.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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.model
+
+import com.adyen.checkout.components.core.internal.ui.model.FieldState
+import com.adyen.checkout.components.core.internal.ui.model.OutputData
+import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState
+import com.adyen.checkout.ui.core.internal.ui.model.AddressOutputData
+
+internal data class BoletoOutputData(
+ val firstNameState: FieldState,
+ val lastNameState: FieldState,
+ val socialSecurityNumberState: FieldState,
+ val addressState: AddressOutputData,
+ val addressUIState: AddressFormUIState,
+ val isEmailVisible: Boolean,
+ val isSendEmailSelected: Boolean,
+ val shopperEmailState: FieldState
+) : OutputData {
+
+ override val isValid: Boolean
+ get() = firstNameState.validation.isValid() &&
+ lastNameState.validation.isValid() &&
+ socialSecurityNumberState.validation.isValid() &&
+ addressState.isValid &&
+ shopperEmailState.validation.isValid()
+}
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/view/BoletoView.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/view/BoletoView.kt
new file mode 100644
index 0000000000..f806eb0be9
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/ui/view/BoletoView.kt
@@ -0,0 +1,228 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.view
+
+import android.content.Context
+import android.text.Editable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnFocusChangeListener
+import android.widget.LinearLayout
+import androidx.core.view.isVisible
+import com.adyen.checkout.boleto.R
+import com.adyen.checkout.boleto.databinding.BoletoViewBinding
+import com.adyen.checkout.boleto.internal.ui.BoletoDelegate
+import com.adyen.checkout.components.core.internal.ui.ComponentDelegate
+import com.adyen.checkout.components.core.internal.ui.model.Validation
+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.hideKeyboard
+import com.adyen.checkout.ui.core.internal.util.isVisible
+import com.adyen.checkout.ui.core.internal.util.setLocalizedHintFromStyle
+import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle
+import com.adyen.checkout.ui.core.internal.util.showError
+import com.adyen.checkout.ui.core.internal.util.showKeyboard
+import kotlinx.coroutines.CoroutineScope
+
+@Suppress("TooManyFunctions")
+internal class BoletoView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : LinearLayout(context, attrs, defStyleAttr), ComponentView {
+
+ private val binding = BoletoViewBinding.inflate(LayoutInflater.from(context), this)
+
+ private lateinit var localizedContext: Context
+
+ private lateinit var boletoDelegate: BoletoDelegate
+
+ init {
+ orientation = VERTICAL
+
+ val padding = resources.getDimension(R.dimen.standard_margin).toInt()
+ setPadding(padding, padding, padding, 0)
+ }
+
+ override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
+ require(delegate is BoletoDelegate) { "Unsupported delegate type" }
+ boletoDelegate = delegate
+
+ this.localizedContext = localizedContext
+
+ initLocalizedStrings(localizedContext)
+ initFirstNameInput()
+ initLastNameInput()
+ initSocialSecurityNumberInput()
+ initAddressFormInput(coroutineScope)
+ initEmail(delegate.outputData.isEmailVisible)
+ }
+
+ private fun initLocalizedStrings(localizedContext: Context) {
+ binding.textViewPersonalInformationHeader.setLocalizedTextFromStyle(
+ R.style.AdyenCheckout_Boleto_PersonalDetailsHeader,
+ localizedContext
+ )
+ binding.textInputLayoutFirstName.setLocalizedHintFromStyle(
+ R.style.AdyenCheckout_Boleto_FirstNameInput,
+ localizedContext
+ )
+ binding.textInputLayoutLastName.setLocalizedHintFromStyle(
+ R.style.AdyenCheckout_Boleto_LastNameInput,
+ localizedContext
+ )
+ binding.textInputLayoutSocialSecurityNumber.setLocalizedHintFromStyle(
+ R.style.AdyenCheckout_Boleto_SocialNumberInput,
+ localizedContext
+ )
+ binding.addressFormInput.initLocalizedContext(localizedContext)
+ binding.switchSendEmailCopy.setLocalizedTextFromStyle(
+ R.style.AdyenCheckout_Boleto_EmailCopySwitch,
+ localizedContext
+ )
+ binding.textInputLayoutShopperEmail.setLocalizedHintFromStyle(
+ R.style.AdyenCheckout_Boleto_ShopperEmailInput,
+ localizedContext
+ )
+ }
+
+ private fun initFirstNameInput() {
+ binding.editTextFirstName.setOnChangeListener { editable: Editable ->
+ boletoDelegate.updateInputData { firstName = editable.toString() }
+ binding.textInputLayoutFirstName.hideError()
+ }
+ binding.editTextFirstName.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
+ val firstNameValidation = boletoDelegate.outputData.firstNameState.validation
+ if (hasFocus) {
+ binding.textInputLayoutFirstName.hideError()
+ } else if (firstNameValidation is Validation.Invalid) {
+ binding.textInputLayoutFirstName.showError(localizedContext.getString(firstNameValidation.reason))
+ }
+ }
+ }
+
+ private fun initLastNameInput() {
+ binding.editTextLastName.setOnChangeListener { editable: Editable ->
+ boletoDelegate.updateInputData { lastName = editable.toString() }
+ binding.textInputLayoutLastName.hideError()
+ }
+ binding.editTextLastName.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
+ val lastNameValidation = boletoDelegate.outputData.lastNameState.validation
+ if (hasFocus) {
+ binding.textInputLayoutLastName.hideError()
+ } else if (lastNameValidation is Validation.Invalid) {
+ binding.textInputLayoutLastName.showError(localizedContext.getString(lastNameValidation.reason))
+ }
+ }
+ }
+
+ private fun initSocialSecurityNumberInput() {
+ binding.editTextSocialSecurityNumber.setOnChangeListener { editable ->
+ boletoDelegate.updateInputData { socialSecurityNumber = editable.toString() }
+ binding.textInputLayoutSocialSecurityNumber.hideError()
+ }
+ binding.editTextSocialSecurityNumber.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
+ val socialSecurityNumberValidation = boletoDelegate.outputData.socialSecurityNumberState.validation
+ if (hasFocus) {
+ binding.textInputLayoutSocialSecurityNumber.hideError()
+ } else if (socialSecurityNumberValidation is Validation.Invalid) {
+ binding.textInputLayoutSocialSecurityNumber.showError(
+ localizedContext.getString(socialSecurityNumberValidation.reason)
+ )
+ }
+ }
+ }
+
+ private fun initAddressFormInput(coroutineScope: CoroutineScope) {
+ binding.addressFormInput.attachDelegate(boletoDelegate, coroutineScope)
+ }
+
+ private fun initEmail(isEmailVisible: Boolean) {
+ binding.switchSendEmailCopy.isVisible = isEmailVisible
+ if (isEmailVisible) {
+ binding.switchSendEmailCopy.setOnCheckedChangeListener { _, isChecked ->
+ binding.textInputLayoutShopperEmail.isVisible = isChecked
+ if (isChecked) {
+ binding.editTextShopperEmail.requestFocus()
+ binding.editTextShopperEmail.showKeyboard()
+ } else {
+ binding.editTextShopperEmail.clearFocus()
+ hideKeyboard()
+ }
+ boletoDelegate.updateInputData { isSendEmailSelected = isChecked }
+ }
+ initEmailInput()
+ }
+ }
+
+ private fun initEmailInput() {
+ binding.editTextShopperEmail.setOnChangeListener {
+ boletoDelegate.updateInputData { shopperEmail = it.toString().trim() }
+ binding.textInputLayoutShopperEmail.hideError()
+ }
+ binding.editTextShopperEmail.onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
+ val shopperEmailValidation = boletoDelegate.outputData.shopperEmailState.validation
+ if (hasFocus) {
+ binding.textInputLayoutShopperEmail.hideError()
+ } else if (shopperEmailValidation is Validation.Invalid) {
+ binding.textInputLayoutShopperEmail.showError(localizedContext.getString(shopperEmailValidation.reason))
+ }
+ }
+ }
+
+ @Suppress("CyclomaticComplexMethod")
+ override fun highlightValidationErrors() {
+ boletoDelegate.outputData.let {
+ var isErrorFocused = false
+ val firstNameValidation = it.firstNameState.validation
+ if (binding.textInputLayoutFirstName.isVisible && firstNameValidation is Validation.Invalid) {
+ isErrorFocused = true
+ binding.textInputLayoutFirstName.requestFocus()
+ binding.textInputLayoutFirstName.showError(localizedContext.getString(firstNameValidation.reason))
+ }
+ val lastNameValidation = it.lastNameState.validation
+ if (binding.textInputLayoutLastName.isVisible && lastNameValidation is Validation.Invalid) {
+ if (!isErrorFocused) {
+ isErrorFocused = true
+ binding.textInputLayoutLastName.requestFocus()
+ }
+ binding.textInputLayoutLastName.showError(localizedContext.getString(lastNameValidation.reason))
+ }
+ val socialSecurityNumberValidation = it.socialSecurityNumberState.validation
+ if (binding.textInputLayoutSocialSecurityNumber.isVisible &&
+ socialSecurityNumberValidation is Validation.Invalid
+ ) {
+ if (!isErrorFocused) {
+ isErrorFocused = true
+ binding.textInputLayoutSocialSecurityNumber.requestFocus()
+ }
+ binding.textInputLayoutSocialSecurityNumber.showError(
+ localizedContext.getString(socialSecurityNumberValidation.reason)
+ )
+ }
+ if (binding.addressFormInput.isVisible && !it.addressState.isValid) {
+ binding.addressFormInput.highlightValidationErrors(isErrorFocused)
+ }
+ val shopperEmailValidation = it.shopperEmailState.validation
+ if (shopperEmailValidation is Validation.Invalid && binding.textInputLayoutShopperEmail.isVisible) {
+ if (!isErrorFocused) {
+ @Suppress("UNUSED_VALUE")
+ isErrorFocused = true
+ binding.textInputLayoutShopperEmail.requestFocus()
+ }
+ binding.textInputLayoutShopperEmail.showError(
+ localizedContext.getString(shopperEmailValidation.reason)
+ )
+ }
+ }
+ }
+
+ override fun getView(): View = this
+}
diff --git a/boleto/src/main/java/com/adyen/checkout/boleto/internal/util/BoletoValidationUtils.kt b/boleto/src/main/java/com/adyen/checkout/boleto/internal/util/BoletoValidationUtils.kt
new file mode 100644
index 0000000000..496dfac927
--- /dev/null
+++ b/boleto/src/main/java/com/adyen/checkout/boleto/internal/util/BoletoValidationUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * 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 atef on 14/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.util
+
+import com.adyen.checkout.boleto.R
+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
+
+internal object BoletoValidationUtils {
+
+ fun validateFirstName(firstName: String): FieldState {
+ return if (firstName.isNotBlank()) {
+ FieldState(firstName, Validation.Valid)
+ } else {
+ FieldState(firstName, Validation.Invalid(R.string.checkout_boleto_first_name_invalid))
+ }
+ }
+
+ fun validateLastName(lastName: String): FieldState {
+ return if (lastName.isNotBlank()) {
+ FieldState(lastName, Validation.Valid)
+ } else {
+ FieldState(lastName, Validation.Invalid(R.string.checkout_boleto_last_name_invalid))
+ }
+ }
+
+ fun validateShopperEmail(isEmailEnabled: Boolean, shopperEmail: String): FieldState {
+ return if (!isEmailEnabled || ValidationUtils.isEmailValid(shopperEmail)) {
+ FieldState(shopperEmail, Validation.Valid)
+ } else {
+ FieldState(shopperEmail, Validation.Invalid(R.string.checkout_boleto_email_invalid))
+ }
+ }
+}
diff --git a/boleto/src/main/res/layout/boleto_view.xml b/boleto/src/main/res/layout/boleto_view.xml
new file mode 100644
index 0000000000..f46d8f7efb
--- /dev/null
+++ b/boleto/src/main/res/layout/boleto_view.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/boleto/src/main/res/template/values/strings.xml.tt b/boleto/src/main/res/template/values/strings.xml.tt
new file mode 100644
index 0000000000..95fbe06ebc
--- /dev/null
+++ b/boleto/src/main/res/template/values/strings.xml.tt
@@ -0,0 +1,21 @@
+
+
+
+ %%personalDetails%%
+ %%firstName%%
+ %%lastName%%
+ %%boleto.sendCopyToEmail%%
+ %%shopperEmail%%
+ %%boletobancario.btnLabel%%
+ CPF/CNPJ
+
+ %%firstName.invalid%%
+ %%lastName.invalid%%
+ %%shopperEmail.invalid%%
+
diff --git a/boleto/src/main/res/values-ar/strings.xml b/boleto/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..6335368497
--- /dev/null
+++ b/boleto/src/main/res/values-ar/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ البيانات الشخصية
+ الاسم الأول
+ الاسم الأخير
+ إرسال نسخة إلى بريدي الإلكتروني
+ عنوان البريد الإلكتروني
+ إنشاء طريقة دفع Boleto
+
+ الاسم الأول غير صحيح
+ الاسم الأخير غير صحيح
+ عنوان بريد إلكتروني غير صحيح
+
diff --git a/boleto/src/main/res/values-cs-rCZ/strings.xml b/boleto/src/main/res/values-cs-rCZ/strings.xml
new file mode 100644
index 0000000000..d3d44aef2f
--- /dev/null
+++ b/boleto/src/main/res/values-cs-rCZ/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Osobní údaje
+ Jméno
+ Příjmení
+ Poslat mi kopii na e-mail
+ E-mailová adresa
+ Vygenerovat Boleto
+
+ Křestní jméno není platné
+ Příjmení není platné
+ Neplatná e-mailová adresa
+
diff --git a/boleto/src/main/res/values-da-rDK/strings.xml b/boleto/src/main/res/values-da-rDK/strings.xml
new file mode 100644
index 0000000000..48e5562277
--- /dev/null
+++ b/boleto/src/main/res/values-da-rDK/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Personlige oplysninger
+ Fornavn
+ Efternavn
+ Send en kopi til min e-mail
+ E-mailadresse
+ Generér Boleto
+
+ Fornavnet er ikke gyldigt
+ Efternavnet er ikke gyldigt
+ Ugyldig e-mailadresse
+
diff --git a/boleto/src/main/res/values-de-rDE/strings.xml b/boleto/src/main/res/values-de-rDE/strings.xml
new file mode 100644
index 0000000000..5d0bc626a6
--- /dev/null
+++ b/boleto/src/main/res/values-de-rDE/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Persönliche Angaben
+ Vorname
+ Nachname
+ Eine Kopie an meine E-Mail-Adresse senden
+ E-Mail-Adresse
+ Boleto generieren
+
+ Vorname ist ungültig
+ Nachname ist ungültig
+ Ungültige E-Mail-Adresse
+
diff --git a/boleto/src/main/res/values-el-rGR/strings.xml b/boleto/src/main/res/values-el-rGR/strings.xml
new file mode 100644
index 0000000000..b906857ddc
--- /dev/null
+++ b/boleto/src/main/res/values-el-rGR/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Προσωπικά στοιχεία
+ Όνομα
+ Επώνυμο
+ Αποστολή αντιγράφου στη διεύθυνση email μου
+ Διεύθυνση email
+ Δημιουργία Boleto
+
+ Το όνομα δεν είναι έγκυρο
+ Το επώνυμο δεν είναι έγκυρο
+ Μη έγκυρη διεύθυνση email
+
diff --git a/boleto/src/main/res/values-es-rES/strings.xml b/boleto/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..c6c7d88a5b
--- /dev/null
+++ b/boleto/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Datos personales
+ Nombre
+ Apellidos
+ Enviar copia a mi correo electrónico
+ Dirección de correo electrónico
+ Generar Boleto
+
+ El nombre no es válido
+ El apellido no es válido
+ La dirección de correo electrónico no es válida
+
diff --git a/boleto/src/main/res/values-fi-rFI/strings.xml b/boleto/src/main/res/values-fi-rFI/strings.xml
new file mode 100644
index 0000000000..de33f3014a
--- /dev/null
+++ b/boleto/src/main/res/values-fi-rFI/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Henkilötiedot
+ Etunimi
+ Sukunimi
+ Lähetä kopio sähköpostiini
+ Sähköpostiosoite
+ Luo Boleto
+
+ Etunimi ei ole kelvollinen
+ Sukunimi ei ole kelvollinen
+ Ei-kelvollinen sähköpostiosoite
+
diff --git a/boleto/src/main/res/values-fr-rFR/strings.xml b/boleto/src/main/res/values-fr-rFR/strings.xml
new file mode 100644
index 0000000000..a345e42146
--- /dev/null
+++ b/boleto/src/main/res/values-fr-rFR/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Informations personnelles
+ Prénom
+ Nom de famille
+ Envoyer une copie à mon adresse e-mail
+ Adresse e-mail
+ Générer un Boleto
+
+ Le prénom n\'est pas valide
+ Le nom n\'est pas valide
+ Adresse e-mail incorrecte
+
diff --git a/boleto/src/main/res/values-hr-rHR/strings.xml b/boleto/src/main/res/values-hr-rHR/strings.xml
new file mode 100644
index 0000000000..1bcc43556b
--- /dev/null
+++ b/boleto/src/main/res/values-hr-rHR/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Osobni podatci
+ Ime
+ Prezime
+ Pošalji kopiju na moju e-poštu
+ Adresa e-pošte
+ Generiraj Boleto
+
+ Ime nije valjano
+ Prezime nije valjano
+ Nevažeća adresa e-pošte
+
diff --git a/boleto/src/main/res/values-hu-rHU/strings.xml b/boleto/src/main/res/values-hu-rHU/strings.xml
new file mode 100644
index 0000000000..2db33dc600
--- /dev/null
+++ b/boleto/src/main/res/values-hu-rHU/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Személyes adatok
+ Keresztnév
+ Vezetéknév
+ Másolat küldése az e-mail-címemre
+ E-mail-cím
+ Boleto létrehozása
+
+ A keresztnév nem érvényes
+ A vezetéknév nem érvényes
+ Érvénytelen e-mail-cím
+
diff --git a/boleto/src/main/res/values-it-rIT/strings.xml b/boleto/src/main/res/values-it-rIT/strings.xml
new file mode 100644
index 0000000000..ecd39107e9
--- /dev/null
+++ b/boleto/src/main/res/values-it-rIT/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Dati personali
+ Nome
+ Cognome
+ Invia una copia alla mia e-mail
+ Indirizzo e-mail
+ Genera Boleto
+
+ Nome non valido
+ Cognome non valido
+ Indirizzo e-mail non valido
+
diff --git a/boleto/src/main/res/values-ja-rJP/strings.xml b/boleto/src/main/res/values-ja-rJP/strings.xml
new file mode 100644
index 0000000000..a42176f3b1
--- /dev/null
+++ b/boleto/src/main/res/values-ja-rJP/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ 個人情報
+ 名
+ 姓
+ 自分のメールアドレスにコピーを送信する
+ Eメールアドレス
+ Boletoを生成する
+
+ 名が無効です
+ 姓が無効です
+ Eメールアドレスが無効です
+
diff --git a/boleto/src/main/res/values-ko-rKR/strings.xml b/boleto/src/main/res/values-ko-rKR/strings.xml
new file mode 100644
index 0000000000..f60a0aa9f6
--- /dev/null
+++ b/boleto/src/main/res/values-ko-rKR/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ 개인 정보
+ 이름
+ 성
+ 내 이메일로 사본 보내기
+ 이메일 주소
+ Boleto 생성
+
+ 이름이 올바르지 않습니다
+ 성이 올바르지 않습니다
+ 유효하지 않은 이메일 주소
+
diff --git a/boleto/src/main/res/values-nb-rNO/strings.xml b/boleto/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..c5ac58499c
--- /dev/null
+++ b/boleto/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Personopplysninger
+ Fornavn
+ Etternavn
+ Send meg en kopi på e-post
+ E-postadresse
+ Generer Boleto
+
+ Fornavnet er ikke gyldig
+ Etternavnet er ikke gyldig
+ Ugyldig e-postadresse
+
diff --git a/boleto/src/main/res/values-nl-rNL/strings.xml b/boleto/src/main/res/values-nl-rNL/strings.xml
new file mode 100644
index 0000000000..3063edcee3
--- /dev/null
+++ b/boleto/src/main/res/values-nl-rNL/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Persoonlijke gegevens
+ Voornaam
+ Achternaam
+ Stuur een kopie naar mijn e-mailadres
+ E-mailadres
+ Boleto genereren
+
+ Voornaam is niet geldig
+ Achternaam is niet geldig
+ Ongeldig e-mailadres
+
diff --git a/boleto/src/main/res/values-pl-rPL/strings.xml b/boleto/src/main/res/values-pl-rPL/strings.xml
new file mode 100644
index 0000000000..eb991958b6
--- /dev/null
+++ b/boleto/src/main/res/values-pl-rPL/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Dane osobowe
+ Imię
+ Nazwisko
+ Wyślij kopię na mój e-mail
+ Adres e-mail
+ Wygeneruj płatność Boleto
+
+ Imię jest nieprawidłowe
+ Nazwisko jest nieprawidłowe
+ Niepoprawny adres email
+
diff --git a/boleto/src/main/res/values-pt-rBR/strings.xml b/boleto/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..b1965158c5
--- /dev/null
+++ b/boleto/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Informações pessoais
+ Nome
+ Sobrenome
+ Enviar uma cópia por e-mail
+ Endereço de e-mail
+ Gerar Boleto
+
+ Este não é um nome válido
+ Este não é um sobrenome válido
+ Endereço de e-mail inválido
+
diff --git a/boleto/src/main/res/values-pt-rPT/strings.xml b/boleto/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..f654d0ae45
--- /dev/null
+++ b/boleto/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Detalhes pessoais
+ Nome próprio
+ Apelido
+ Enviar uma cópia para o meu e-mail
+ Endereço de correio eletrónico
+ Gerar comprovativo
+
+ O nome próprio não é válido
+ O apelido não é válido
+ Endereço de e-mail inválido
+
diff --git a/boleto/src/main/res/values-ro-rRO/strings.xml b/boleto/src/main/res/values-ro-rRO/strings.xml
new file mode 100644
index 0000000000..3900d6bb25
--- /dev/null
+++ b/boleto/src/main/res/values-ro-rRO/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Informații personale
+ Prenume
+ Nume de familie
+ Trimite o copie la adresa mea de e-mail
+ Adresă de e-mail
+ Generare Boleto
+
+ Prenumele nu este valabil
+ Numele de familie nu este valabil
+ Adresă de e-mail incorectă
+
diff --git a/boleto/src/main/res/values-ru-rRU/strings.xml b/boleto/src/main/res/values-ru-rRU/strings.xml
new file mode 100644
index 0000000000..220508c8af
--- /dev/null
+++ b/boleto/src/main/res/values-ru-rRU/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Личные данные
+ Имя
+ Фамилия
+ Отправить мне копию на эл. почту
+ Адрес эл. почты
+ Создать Boleto
+
+ Неверное имя
+ Неверная фамилия
+ Недействительный адрес эл. почты
+
diff --git a/boleto/src/main/res/values-sk-rSK/strings.xml b/boleto/src/main/res/values-sk-rSK/strings.xml
new file mode 100644
index 0000000000..e78d170e9b
--- /dev/null
+++ b/boleto/src/main/res/values-sk-rSK/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Osobné údaje
+ Krstné meno
+ Priezvisko
+ Zaslať kópiu na môj e-mail
+ E-mailová adresa
+ Generovať Boleto
+
+ Meno nie je platné
+ Priezvisko nie je platné
+ Neplatná emailová adresa
+
diff --git a/boleto/src/main/res/values-sl-rSI/strings.xml b/boleto/src/main/res/values-sl-rSI/strings.xml
new file mode 100644
index 0000000000..560c324737
--- /dev/null
+++ b/boleto/src/main/res/values-sl-rSI/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Osebni podatki
+ Ime
+ Priimek
+ Pošlji kopijo na moj elektronski naslov
+ Elektronski naslov
+ Ustvari Boleto
+
+ Ime ni veljavno
+ Priimek ni veljaven
+ Neveljaven elektronski naslov
+
diff --git a/boleto/src/main/res/values-sv-rSE/strings.xml b/boleto/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..aea6d591b8
--- /dev/null
+++ b/boleto/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ Personuppgifter
+ Förnamn
+ Efternamn
+ Skicka en kopia till min e-post
+ E-postadress
+ Generera Boleto
+
+ Förnamnet är inte giltigt
+ Efternamnet är inte giltigt
+ Ogiltig e-postadress
+
diff --git a/boleto/src/main/res/values-zh-rCN/strings.xml b/boleto/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..71a382aba5
--- /dev/null
+++ b/boleto/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ 个人详细信息
+ 名字
+ 姓氏
+ 将副本发送到我的电子邮箱
+ 电子邮件地址
+ 生成 Boleto
+
+ 名字无效
+ 姓氏无效
+ 无效的邮件地址
+
diff --git a/boleto/src/main/res/values-zh-rTW/strings.xml b/boleto/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..8027a1cd53
--- /dev/null
+++ b/boleto/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,20 @@
+
+
+
+ 個人詳細資料
+ 名字
+ 姓氏
+ 將複本傳送至我的電子郵件
+ 電子郵件地址
+ 產生 Boleto
+
+ 名字無效
+ 姓氏無效
+ 電子郵件地址無效
+
diff --git a/boleto/src/main/res/values/strings.xml b/boleto/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..acdcc50334
--- /dev/null
+++ b/boleto/src/main/res/values/strings.xml
@@ -0,0 +1,21 @@
+
+
+
+ Personal details
+ First name
+ Last name
+ Send a copy to my email
+ Email address
+ Generate Boleto
+ CPF/CNPJ
+
+ First name is not valid
+ Last name is not valid
+ Invalid email address
+
diff --git a/boleto/src/main/res/values/styles.xml b/boleto/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..e4c346af2b
--- /dev/null
+++ b/boleto/src/main/res/values/styles.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt
new file mode 100644
index 0000000000..8ec78b159c
--- /dev/null
+++ b/boleto/src/test/java/com/adyen/checkout/boleto/BoletoComponentTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.viewModelScope
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
+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.TestDispatcherExtension
+import com.adyen.checkout.test.extensions.invokeOnCleared
+import com.adyen.checkout.test.extensions.test
+import com.adyen.checkout.ui.core.internal.test.TestComponentViewType
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions
+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.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class)
+internal class BoletoComponentTest(
+ @Mock private val boletoDelegate: BoletoDelegate,
+ @Mock private val genericActionDelegate: GenericActionDelegate,
+ @Mock private val actionHandlingComponent: DefaultActionHandlingComponent,
+ @Mock private val componentEventHandler: ComponentEventHandler,
+) {
+ private lateinit var component: BoletoComponent
+
+ @BeforeEach
+ fun beforeEach() {
+ whenever(boletoDelegate.viewFlow) doReturn MutableStateFlow(BoletoComponentViewType)
+ whenever(genericActionDelegate.viewFlow) doReturn MutableStateFlow(null)
+
+ component = BoletoComponent(
+ boletoDelegate = boletoDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = actionHandlingComponent,
+ componentEventHandler = componentEventHandler,
+ )
+
+ AdyenLogger.setLogLevel(Logger.NONE)
+ }
+
+ @Test
+ fun `when component is created then delegates are initialized`() {
+ verify(boletoDelegate).initialize(component.viewModelScope)
+ verify(genericActionDelegate).initialize(component.viewModelScope)
+ verify(componentEventHandler).initialize(component.viewModelScope)
+ }
+
+ @Test
+ fun `when component is cleared then delegates are cleared`() {
+ component.invokeOnCleared()
+
+ verify(boletoDelegate).onCleared()
+ verify(genericActionDelegate).onCleared()
+ verify(componentEventHandler).onCleared()
+ }
+
+ @Test
+ fun `when observe is called then observe in delegates is called`() {
+ val lifecycleOwner = mock()
+ val callback: (PaymentComponentEvent) -> Unit = {}
+
+ component.observe(lifecycleOwner, callback)
+
+ verify(boletoDelegate).observe(lifecycleOwner, component.viewModelScope, callback)
+ verify(genericActionDelegate).observe(eq(lifecycleOwner), eq(component.viewModelScope), any())
+ }
+
+ @Test
+ fun `when removeObserver is called then removeObserver in delegates is called`() {
+ component.removeObserver()
+
+ verify(boletoDelegate).removeObserver()
+ verify(genericActionDelegate).removeObserver()
+ }
+
+ @Test
+ fun `when component is initialized then view flow should match boleto delegate view flow`() = runTest {
+ runCurrent()
+ Assertions.assertEquals(BoletoComponentViewType, component.viewFlow.first())
+ }
+
+ @Test
+ 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(boletoDelegate.viewFlow) doReturn delegateViewFlow
+ component = BoletoComponent(
+ boletoDelegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler
+ )
+
+ val viewTestFlow = component.viewFlow.test(testScheduler)
+ Assertions.assertEquals(TestComponentViewType.VIEW_TYPE_1, viewTestFlow.values.last())
+
+ delegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
+ Assertions.assertEquals(TestComponentViewType.VIEW_TYPE_2, viewTestFlow.values.last())
+
+ viewTestFlow.cancel()
+ }
+
+ @Test
+ fun `when action delegate view flow emits a value then component view flow should match that value`() = runTest {
+ val actionDelegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1)
+ whenever(genericActionDelegate.viewFlow) doReturn actionDelegateViewFlow
+ component = BoletoComponent(
+ boletoDelegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler
+ )
+
+ val viewTestFlow = component.viewFlow.test(testScheduler)
+
+ // this value should match the value of the main delegate and not the action delegate
+ // and in practice the initial value of the action delegate view flow is always null so it should be ignored
+ Assertions.assertEquals(BoletoComponentViewType, viewTestFlow.values.last())
+
+ actionDelegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
+ Assertions.assertEquals(TestComponentViewType.VIEW_TYPE_2, viewTestFlow.values.last())
+
+ viewTestFlow.cancel()
+ }
+}
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
new file mode 100644
index 0000000000..b96b056c93
--- /dev/null
+++ b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/DefaultBoletoDelegateTest.kt
@@ -0,0 +1,545 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+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.internal.ui.model.BoletoComponentParamsMapper
+import com.adyen.checkout.components.core.Amount
+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.core.Environment
+import com.adyen.checkout.core.internal.util.Logger
+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
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+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.BeforeEach
+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.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import org.mockito.Mock
+import org.mockito.junit.jupiter.MockitoExtension
+import org.mockito.kotlin.verify
+import java.util.Locale
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(MockitoExtension::class)
+internal class DefaultBoletoDelegateTest(
+ @Mock private val submitHandler: SubmitHandler,
+ @Mock private val analyticsRepository: AnalyticsRepository,
+) {
+
+ private lateinit var delegate: DefaultBoletoDelegate
+
+ private lateinit var addressRepository: TestAddressRepository
+
+ @BeforeEach
+ fun beforeEach() {
+ addressRepository = TestAddressRepository()
+ delegate = createBoletoDelegate()
+ AdyenLogger.setLogLevel(Logger.NONE)
+ }
+
+ @Test
+ fun `when delegate is initialized then analytics event is sent`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ verify(analyticsRepository).sendAnalyticsEvent()
+ }
+
+ @Nested
+ @DisplayName("when input data changes and")
+ inner class InputDataChangedTest {
+
+ @Test
+ fun `input data is valid, then output must be valid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ assertTrue(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `all inputs are empty, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {}
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `first name is empty and other inputs are valid, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = " "
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `last name is empty and other inputs are valid, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = " "
+ socialSecurityNumber = "568.617.525-09"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `social security number is empty and other inputs are valid, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = " "
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `social security number is invalid and other inputs are valid, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "123.456.789-0"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `address is empty and other inputs are valid, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = AddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `email is empty and isEmailCopySelected equals true, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = AddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = " "
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+
+ @Test
+ fun `email is invalid and isEmailCopySelected equals true, then output should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val outputTestFlow = delegate.outputDataFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = AddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test"
+ }
+
+ assertFalse(outputTestFlow.latestValue.isValid)
+ outputTestFlow.cancel()
+ }
+ }
+
+ @Nested
+ @DisplayName("when creating component state and")
+ inner class CreateComponentStateTest {
+
+ @Test
+ fun `output is valid, then component state should be valid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertTrue(isInputValid)
+ assertTrue(isValid)
+ }
+ }
+
+ @Test
+ fun `output is invalid, then component state should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {}
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `first name is empty and other inputs are valid, then component state should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = " "
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `last name is empty and other inputs are valid, then component state should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = " "
+ socialSecurityNumber = "568.617.525-09"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `social security number is empty and other inputs are valid, then component state should be invalid`() =
+ runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = " "
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `social security number is invalid input and other inputs are valid, then component state should be invalid`() =
+ runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "123.456.789-0"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `social security number is invalid pattern and other inputs are valid, then component state should be invalid`() =
+ runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "56861752509"
+ address = createAddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `address is empty and other inputs are valid, then component state should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = AddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test.com"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `email is empty and isEmailCopySelected equals true, then component state should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = AddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = " "
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @Test
+ fun `email is invalid and isEmailCopySelected equals true, then component state should be invalid`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val componentStateTestFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.updateInputData {
+ firstName = "Atef"
+ lastName = "Etman"
+ socialSecurityNumber = "568.617.525-09"
+ address = AddressInputModel()
+ isSendEmailSelected = true
+ shopperEmail = "atef@test"
+ }
+
+ with(componentStateTestFlow.latestValue) {
+ assertFalse(isInputValid)
+ assertFalse(isValid)
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("com.adyen.checkout.boleto.internal.ui.DefaultBoletoDelegateTest#amountSource")
+ fun `when input data is valid then amount is propagated in component state if set`(
+ configurationValue: Amount?,
+ expectedComponentStateValue: Amount?,
+ ) = runTest {
+ if (configurationValue != null) {
+ val configuration = getDefaultBoletoConfigurationBuilder()
+ .setAmount(configurationValue)
+ .build()
+ delegate = createBoletoDelegate(configuration = configuration)
+ }
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ delegate.componentStateFlow.test {
+ delegate.updateInputData {
+ firstName = "Test"
+ }
+ assertEquals(expectedComponentStateValue, expectMostRecentItem().data.amount)
+ }
+ }
+ }
+
+ @Nested
+ inner class SubmitHandlerTest {
+ @Test
+ fun `when delegate is initialized then submit handler event is initialized`() = runTest {
+ val coroutineScope = CoroutineScope(UnconfinedTestDispatcher())
+ delegate.initialize(coroutineScope)
+ verify(submitHandler).initialize(coroutineScope, delegate.componentStateFlow)
+ }
+
+ @Test
+ fun `when delegate setInteractionBlocked is called then submit handler setInteractionBlocked is called`() =
+ runTest {
+ delegate.setInteractionBlocked(true)
+ verify(submitHandler).setInteractionBlocked(true)
+ }
+
+ @Test
+ fun `when delegate onSubmit is called then submit handler onSubmit is called`() = runTest {
+ delegate.componentStateFlow.test {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ delegate.onSubmit()
+ verify(submitHandler).onSubmit(expectMostRecentItem())
+ }
+ }
+ }
+
+ @Suppress("LongParameterList")
+ private fun createBoletoDelegate(
+ submitHandler: SubmitHandler = this.submitHandler,
+ analyticsRepository: AnalyticsRepository = this.analyticsRepository,
+ paymentMethod: PaymentMethod = PaymentMethod(),
+ addressRepository: TestAddressRepository = this.addressRepository,
+ order: Order? = TEST_ORDER,
+ configuration: BoletoConfiguration = getDefaultBoletoConfigurationBuilder().build(),
+ ) = DefaultBoletoDelegate(
+ submitHandler = submitHandler,
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = paymentMethod,
+ order = order,
+ componentParams = BoletoComponentParamsMapper(null, null).mapToParams(configuration, null),
+ addressRepository = addressRepository
+ )
+
+ @Suppress("LongParameterList")
+ private fun createAddressInputModel(
+ postalCode: String = "12345678",
+ street: String = "Rua Funcionarios",
+ stateOrProvince: String = "SP",
+ houseNumberOrName: String = "952",
+ apartmentSuite: String = "",
+ city: String = "São Paulo",
+ country: String = BRAZIL_COUNTRY_CODE
+ ) = AddressInputModel(
+ postalCode = postalCode,
+ street = street,
+ stateOrProvince = stateOrProvince,
+ houseNumberOrName = houseNumberOrName,
+ apartmentSuite = apartmentSuite,
+ city = city,
+ country = country
+ )
+
+ private fun getDefaultBoletoConfigurationBuilder(): BoletoConfiguration.Builder {
+ return BoletoConfiguration.Builder(Locale.US, Environment.TEST, TEST_CLIENT_KEY)
+ }
+
+ companion object {
+ private const val TEST_CLIENT_KEY = "test_qwertyuiopasdfghjklzxcvbnmqwerty"
+ private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA")
+ private const val BRAZIL_COUNTRY_CODE = "BR"
+
+ @JvmStatic
+ fun amountSource() = listOf(
+ // configurationValue, expectedComponentStateValue
+ Arguments.arguments(Amount("EUR", 100), Amount("EUR", 100)),
+ Arguments.arguments(Amount("USD", 0), Amount("USD", 0)),
+ Arguments.arguments(Amount.EMPTY, null),
+ Arguments.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
new file mode 100644
index 0000000000..9f790fb0c3
--- /dev/null
+++ b/boleto/src/test/java/com/adyen/checkout/boleto/internal/ui/model/BoletoComponentParamsMapperTest.kt
@@ -0,0 +1,192 @@
+/*
+ * 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 atef on 31/3/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.ui.model
+
+import com.adyen.checkout.boleto.BoletoConfiguration
+import com.adyen.checkout.components.core.Amount
+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.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.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()
+
+ val params = getBoletoComponentParamsMapper().mapToParams(boletoConfiguration, null)
+ 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()
+
+ val params = getBoletoComponentParamsMapper().mapToParams(boletoConfiguration, null)
+ val expectedAddressParams = AddressParams.FullAddress(
+ defaultCountryCode = BRAZIL_COUNTRY_CODE,
+ supportedCountryCodes = SUPPORTED_COUNTRY_LIST_1,
+ addressFieldPolicy = AddressFieldPolicyParams.Required
+ )
+ val expected = getBoletoComponentParams(
+ addressParams = expectedAddressParams,
+ 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(
+ shopperLocale = Locale.GERMAN,
+ environment = Environment.EUROPE,
+ clientKey = TEST_CLIENT_KEY_2,
+ isAnalyticsEnabled = false,
+ isCreatedByDropIn = true,
+ amount = Amount(
+ currency = "CAD",
+ value = 123_00L
+ )
+ )
+
+ val params = getBoletoComponentParamsMapper(overrideComponentParams = overrideComponentParams).mapToParams(
+ boletoConfiguration,
+ null
+ )
+
+ val expected = getBoletoComponentParams(
+ shopperLocale = Locale.GERMAN,
+ environment = Environment.EUROPE,
+ clientKey = TEST_CLIENT_KEY_2,
+ isAnalyticsEnabled = false,
+ isCreatedByDropIn = true,
+ amount = Amount(
+ currency = "CAD",
+ value = 123_00L
+ )
+ )
+
+ assertEquals(expected, params)
+ }
+
+ @Test
+ fun `when send email is set, them params should match`() {
+ val boletoConfiguration = getBoletoConfigurationBuilder()
+ .setEmailVisibility(true)
+ .build()
+
+ val params = getBoletoComponentParamsMapper().mapToParams(boletoConfiguration, null)
+ val expected = getBoletoComponentParams(
+ isSendEmailVisible = true
+ )
+
+ 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`(
+ configurationValue: Amount,
+ dropInValue: Amount?,
+ sessionsValue: Amount?,
+ expectedValue: Amount
+ ) {
+ val boletoConfiguration = getBoletoConfigurationBuilder()
+ .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 { getBoletoComponentParams(amount = it) }
+
+ val params = getBoletoComponentParamsMapper(overrideComponentParams = overrideParams).mapToParams(
+ boletoConfiguration,
+ sessionParams = SessionParams(
+ enableStoreDetails = null,
+ installmentOptions = null,
+ amount = sessionsValue,
+ returnUrl = "",
+ )
+ )
+
+ val expected = getBoletoComponentParams(
+ amount = expectedValue
+ )
+
+ assertEquals(expected, params)
+ }
+
+ private fun getBoletoConfigurationBuilder() = BoletoConfiguration.Builder(
+ shopperLocale = Locale.US,
+ environment = Environment.TEST,
+ clientKey = TEST_CLIENT_KEY_1,
+ )
+
+ @Suppress("LongParameterList")
+ private fun getBoletoComponentParams(
+ isSubmitButtonVisible: Boolean = true,
+ shopperLocale: Locale = Locale.US,
+ environment: Environment = Environment.TEST,
+ clientKey: String = TEST_CLIENT_KEY_1,
+ isAnalyticsEnabled: Boolean = true,
+ isCreatedByDropIn: Boolean = false,
+ amount: Amount = Amount.EMPTY,
+ addressParams: AddressParams = AddressParams.FullAddress(
+ defaultCountryCode = BRAZIL_COUNTRY_CODE,
+ supportedCountryCodes = SUPPORTED_COUNTRY_LIST_1,
+ addressFieldPolicy = AddressFieldPolicyParams.Required
+ ),
+ isSendEmailVisible: Boolean = false
+ ) = BoletoComponentParams(
+ isSubmitButtonVisible = isSubmitButtonVisible,
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey,
+ isAnalyticsEnabled = isAnalyticsEnabled,
+ isCreatedByDropIn = isCreatedByDropIn,
+ amount = amount,
+ addressParams = addressParams,
+ isEmailVisible = isSendEmailVisible
+ )
+
+ private fun getBoletoComponentParamsMapper(
+ overrideComponentParams: ComponentParams? = null,
+ overrideSessionParams: SessionParams? = null,
+ ) = BoletoComponentParamsMapper(overrideComponentParams, overrideSessionParams)
+
+ 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)
+
+ @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)),
+ )
+ }
+}
diff --git a/boleto/src/test/java/com/adyen/checkout/boleto/internal/util/BoletoValidationUtilsTest.kt b/boleto/src/test/java/com/adyen/checkout/boleto/internal/util/BoletoValidationUtilsTest.kt
new file mode 100644
index 0000000000..2ebd20584a
--- /dev/null
+++ b/boleto/src/test/java/com/adyen/checkout/boleto/internal/util/BoletoValidationUtilsTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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 atef on 4/4/2023.
+ */
+
+package com.adyen.checkout.boleto.internal.util
+
+import com.adyen.checkout.boleto.R
+import com.adyen.checkout.components.core.internal.ui.model.Validation
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+
+internal class BoletoValidationUtilsTest {
+
+ @ParameterizedTest
+ @MethodSource("firstNameSource")
+ fun `first name value is set, then validation should match expected validation`(
+ firstName: String,
+ expectedValidation: Validation
+ ) {
+ assertEquals(expectedValidation, BoletoValidationUtils.validateFirstName(firstName).validation)
+ }
+
+ @ParameterizedTest
+ @MethodSource("lastNameSource")
+ fun `last name value is set, then validation should match expected validation`(
+ lastName: String,
+ expectedValidation: Validation
+ ) {
+ assertEquals(expectedValidation, BoletoValidationUtils.validateLastName(lastName).validation)
+ }
+
+ @ParameterizedTest
+ @MethodSource("emailSource")
+ fun `email and isEmailEnabled value is set, then actual validation should match expected validation`(
+ isEmailEnabled: Boolean,
+ email: String,
+ expectedValidation: Validation
+ ) {
+ assertEquals(expectedValidation, BoletoValidationUtils.validateShopperEmail(isEmailEnabled, email).validation)
+ }
+
+ companion object {
+ @JvmStatic
+ fun firstNameSource() = listOf(
+ // firstName, expected validation
+ Arguments.arguments("firstname", Validation.Valid),
+ Arguments.arguments("", Validation.Invalid(reason = R.string.checkout_boleto_first_name_invalid)),
+ )
+
+ @JvmStatic
+ fun lastNameSource() = listOf(
+ // lastName, expected validation
+ Arguments.arguments("firstname", Validation.Valid),
+ Arguments.arguments("", Validation.Invalid(reason = R.string.checkout_boleto_last_name_invalid)),
+ )
+
+ @JvmStatic
+ fun emailSource() = listOf(
+ // isEmailEnabled, email, expected validation
+ Arguments.arguments(false, "email", Validation.Valid),
+ Arguments.arguments(false, "email@tezt.com", Validation.Valid),
+ Arguments.arguments(true, "", Validation.Invalid(reason = R.string.checkout_boleto_email_invalid)),
+ Arguments.arguments(true, "email", Validation.Invalid(reason = R.string.checkout_boleto_email_invalid)),
+ Arguments.arguments(true, "email@test.com", Validation.Valid)
+ )
+ }
+}
diff --git a/build.gradle b/build.gradle
index e7de74e7d7..898986ba8d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,36 +1,23 @@
-apply from: "config/gradle/checksums.gradle"
-apply from: "config/gradle/dependenciesCheck.gradle"
-
-ext {
- checkoutRedirectScheme = "adyencheckout"
-}
-
buildscript {
apply from: './dependencies.gradle'
+}
- repositories {
- google()
- mavenCentral()
- maven { url "https://plugins.gradle.org/m2/" }
- }
-
- dependencies {
- classpath "com.android.tools.build:gradle:$android_gradle_plugin_version"
- classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detekt_gradle_plugin_version"
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
- classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
- }
+plugins {
+ id 'com.android.application' version "$android_gradle_plugin_version" apply false
+ id 'com.android.library' version "$android_gradle_plugin_version" apply false
+ id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
+ id 'com.google.dagger.hilt.android' version "$hilt_version" apply false
+ id "io.gitlab.arturbosch.detekt" version "$detekt_gradle_plugin_version"
+ id "org.jetbrains.dokka" version "$dokka_version"
}
apply from: "config/gradle/dokkaRoot.gradle"
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
+ext {
+ checkoutRedirectScheme = "adyencheckout"
+}
+allprojects {
tasks.withType(Test) {
useJUnitPlatform()
}
diff --git a/card/build.gradle b/card/build.gradle
index 485bde156e..d1b10a6949 100644
--- a/card/build.gradle
+++ b/card/build.gradle
@@ -44,7 +44,8 @@ android {
dependencies {
// Checkout
- api project(':action')
+ api project(':3ds2')
+ api project(':action-core')
api project(':cse')
api project(':ui-core')
api project(':sessions-core')
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 eff163a37f..b260b19c87 100644
--- a/card/src/main/java/com/adyen/checkout/card/CardComponent.kt
+++ b/card/src/main/java/com/adyen/checkout/card/CardComponent.kt
@@ -11,9 +11,9 @@ package com.adyen.checkout.card
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.adyen.checkout.action.internal.ActionHandlingComponent
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.ActionHandlingComponent
+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.PaymentMethodTypes
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 4ab31f77f6..1e3048807c 100644
--- a/card/src/main/java/com/adyen/checkout/card/CardConfiguration.kt
+++ b/card/src/main/java/com/adyen/checkout/card/CardConfiguration.kt
@@ -8,9 +8,10 @@
package com.adyen.checkout.card
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.ActionHandlingPaymentMethodConfigurationBuilder
+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.PaymentComponentData
import com.adyen.checkout.components.core.PaymentMethod
import com.adyen.checkout.components.core.internal.ButtonConfiguration
import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
@@ -148,8 +149,7 @@ class CardConfiguration private constructor(
/**
* Set the unique reference for the shopper doing this transaction.
- * This value will simply be passed back to you in the
- * [com.adyen.checkout.components.model.payments.request.PaymentComponentData] for convenience.
+ * This value will simply be passed back to you in the [PaymentComponentData] for convenience.
*
* @param shopperReference The unique shopper reference
* @return [CardConfiguration.Builder]
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 b490ab1021..719af220f6 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
@@ -13,8 +13,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import androidx.savedstate.SavedStateRegistryOwner
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider
import com.adyen.checkout.card.CardComponent
import com.adyen.checkout.card.CardComponentState
import com.adyen.checkout.card.CardConfiguration
@@ -63,8 +63,9 @@ 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
+class CardComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class CardComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) :
@@ -72,22 +73,26 @@ class CardComponentProvider(
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(),
@@ -164,6 +169,7 @@ class CardComponentProvider(
componentEventHandler = DefaultComponentEventHandler(),
)
}
+
return ViewModelProvider(viewModelStoreOwner, factory)[key, CardComponent::class.java].also { component ->
component.observe(lifecycleOwner) {
component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
@@ -322,6 +328,7 @@ class CardComponentProvider(
componentEventHandler = DefaultComponentEventHandler(),
)
}
+
return ViewModelProvider(viewModelStoreOwner, factory)[key, CardComponent::class.java].also { component ->
component.observe(lifecycleOwner) {
component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
@@ -406,6 +413,7 @@ class CardComponentProvider(
componentEventHandler = sessionComponentEventHandler,
)
}
+
return ViewModelProvider(viewModelStoreOwner, factory)[key, CardComponent::class.java].also { component ->
component.observe(lifecycleOwner) {
component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
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 6881c179b1..3a848ac5a6 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
@@ -31,7 +31,6 @@ import com.adyen.checkout.card.internal.util.CardValidationUtils
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.SocialSecurityNumberUtils
import com.adyen.checkout.components.core.OrderRequest
import com.adyen.checkout.components.core.PaymentComponentData
import com.adyen.checkout.components.core.PaymentMethod
@@ -49,6 +48,7 @@ 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.runCompileOnly
import com.adyen.checkout.cse.EncryptedCard
import com.adyen.checkout.cse.EncryptionException
import com.adyen.checkout.cse.UnencryptedCard
@@ -67,6 +67,7 @@ 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 com.adyen.checkout.ui.core.internal.util.AddressValidationUtils
+import com.adyen.checkout.ui.core.internal.util.SocialSecurityNumberUtils
import com.adyen.threeds2.ThreeDS2Service
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
@@ -597,6 +598,7 @@ internal class DefaultCardDelegate(
// is typing the card number.
cvcPolicy == Brand.FieldPolicy.OPTIONAL ||
cvcPolicy == Brand.FieldPolicy.HIDDEN -> InputFieldUIState.OPTIONAL
+
else -> InputFieldUIState.REQUIRED
}
}
@@ -645,21 +647,11 @@ internal class DefaultCardDelegate(
taxNumber = stateOutputData.kcpBirthDateOrTaxNumberState.value
}
- if (isDualBrandedFlow(stateOutputData.detectedCardTypes)) {
- brand = DetectedCardTypesUtils.getSelectedCardType(
- detectedCardTypes = stateOutputData.detectedCardTypes
- )?.cardBrand?.txVariant
- }
+ brand = getCardBrand(stateOutputData.detectedCardTypes)
fundingSource = getFundingSource()
- try {
- threeDS2SdkVersion = ThreeDS2Service.INSTANCE.sdkVersion
- } catch (e: ClassNotFoundException) {
- Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.")
- } catch (e: NoClassDefFoundError) {
- Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.")
- }
+ threeDS2SdkVersion = runCompileOnly { ThreeDS2Service.INSTANCE.sdkVersion }
}
val paymentComponentData = makePaymentComponentData(cardPaymentMethod, stateOutputData)
@@ -733,6 +725,18 @@ internal class DefaultCardDelegate(
}
}
+ private fun getCardBrand(detectedCardTypes: List): String? {
+ return if (isDualBrandedFlow(detectedCardTypes)) {
+ DetectedCardTypesUtils.getSelectedCardType(
+ detectedCardTypes = detectedCardTypes
+ )
+ } else {
+ val reliableCardBrand = detectedCardTypes.firstOrNull { it.isReliable }
+ val firstDetectedBrand = detectedCardTypes.firstOrNull()
+ reliableCardBrand ?: firstDetectedBrand
+ }?.cardBrand?.txVariant
+ }
+
override fun isConfirmationRequired(): Boolean = _viewFlow.value is ButtonComponentViewType
override fun shouldShowSubmitButton(): Boolean = isConfirmationRequired() && componentParams.isSubmitButtonVisible
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 61a4f2caa4..b4aacb486b 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
@@ -38,6 +38,7 @@ 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.runCompileOnly
import com.adyen.checkout.cse.EncryptedCard
import com.adyen.checkout.cse.EncryptionException
import com.adyen.checkout.cse.UnencryptedCard
@@ -332,16 +333,7 @@ internal class StoredCardDelegate(
encryptedSecurityCode = encryptedCard.encryptedSecurityCode
}
- try {
- // This call will throw an exception in case the merchant did not include our 3DS2 component/SDK
- // in their app. They can opt to use the standalone card component without 3DS2 or with another 3DS2
- // library.
- threeDS2SdkVersion = ThreeDS2Service.INSTANCE.sdkVersion
- } catch (e: ClassNotFoundException) {
- Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.")
- } catch (e: NoClassDefFoundError) {
- Logger.e(TAG, "threeDS2SdkVersion not set because 3DS2 SDK is not present in project.")
- }
+ threeDS2SdkVersion = runCompileOnly { ThreeDS2Service.INSTANCE.sdkVersion }
}
val paymentComponentData = makePaymentComponentData(cardPaymentMethod)
@@ -394,6 +386,7 @@ internal class StoredCardDelegate(
// is typing the card number.
cvcPolicy == Brand.FieldPolicy.OPTIONAL ||
cvcPolicy == Brand.FieldPolicy.HIDDEN -> InputFieldUIState.OPTIONAL
+
else -> InputFieldUIState.REQUIRED
}
}
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 523e1e2ff9..c9bd84b4e8 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
@@ -98,7 +98,7 @@ internal class CardView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is CardDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is CardDelegate) { "Unsupported delegate type" }
cardDelegate = delegate
this.localizedContext = localizedContext
diff --git a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/StoredCardView.kt b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/StoredCardView.kt
index b8c892e41b..941f2f113d 100644
--- a/card/src/main/java/com/adyen/checkout/card/internal/ui/view/StoredCardView.kt
+++ b/card/src/main/java/com/adyen/checkout/card/internal/ui/view/StoredCardView.kt
@@ -81,7 +81,7 @@ internal class StoredCardView @JvmOverloads constructor(
}
override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
- if (delegate !is CardDelegate) throw IllegalArgumentException("Unsupported delegate type")
+ require(delegate is CardDelegate) { "Unsupported delegate type" }
cardDelegate = delegate
this.localizedContext = localizedContext
diff --git a/card/src/main/res/layout/card_view.xml b/card/src/main/res/layout/card_view.xml
index ee62c13b3d..8ba391e68c 100644
--- a/card/src/main/res/layout/card_view.xml
+++ b/card/src/main/res/layout/card_view.xml
@@ -156,7 +156,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
-
+
+
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt
new file mode 100644
index 0000000000..d382c9906f
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponent.kt
@@ -0,0 +1,120 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay
+
+import android.content.Context
+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.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
+import com.adyen.checkout.cashapppay.internal.provider.CashAppPayComponentProvider
+import com.adyen.checkout.cashapppay.internal.ui.CashAppPayDelegate
+import com.adyen.checkout.cashapppay.internal.ui.DefaultCashAppPayDelegate
+import com.adyen.checkout.components.core.PaymentMethodTypes
+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.ui.core.internal.ui.ButtonDelegate
+import com.adyen.checkout.ui.core.internal.ui.ComponentViewType
+import com.adyen.checkout.ui.core.internal.ui.ViewableComponent
+import com.adyen.checkout.ui.core.internal.util.mergeViewFlows
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A [PaymentComponent] that supports the [PaymentMethodTypes.CASH_APP_PAY] payment method.
+ */
+class CashAppPayComponent internal constructor(
+ private val cashAppPayDelegate: CashAppPayDelegate,
+ private val genericActionDelegate: GenericActionDelegate,
+ private val actionHandlingComponent: DefaultActionHandlingComponent,
+ internal val componentEventHandler: ComponentEventHandler,
+) : ViewModel(),
+ PaymentComponent,
+ ViewableComponent,
+ ButtonComponent,
+ ActionHandlingComponent by actionHandlingComponent {
+
+ override val delegate: ComponentDelegate get() = actionHandlingComponent.activeDelegate
+
+ override val viewFlow: Flow = mergeViewFlows(
+ viewModelScope,
+ cashAppPayDelegate.viewFlow,
+ genericActionDelegate.viewFlow,
+ )
+
+ init {
+ cashAppPayDelegate.initialize(viewModelScope)
+ genericActionDelegate.initialize(viewModelScope)
+ componentEventHandler.initialize(viewModelScope)
+ }
+
+ internal fun observe(
+ lifecycleOwner: LifecycleOwner,
+ callback: (PaymentComponentEvent) -> Unit
+ ) {
+ cashAppPayDelegate.observe(lifecycleOwner, viewModelScope, callback)
+ genericActionDelegate.observe(lifecycleOwner, viewModelScope, callback.toActionCallback())
+ }
+
+ internal fun removeObserver() {
+ cashAppPayDelegate.removeObserver()
+ genericActionDelegate.removeObserver()
+ }
+
+ override fun setInteractionBlocked(isInteractionBlocked: Boolean) {
+ (delegate as? DefaultCashAppPayDelegate)?.setInteractionBlocked(isInteractionBlocked)
+ ?: Logger.e(TAG, "Payment component is not interactable, ignoring.")
+ }
+
+ override fun isConfirmationRequired(): Boolean =
+ (cashAppPayDelegate as? ButtonDelegate)?.isConfirmationRequired() ?: false
+
+ override fun submit() {
+ (delegate as? ButtonDelegate)?.onSubmit()
+ ?: Logger.e(TAG, "Component is currently not submittable, ignoring.")
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ Logger.d(TAG, "onCleared")
+ cashAppPayDelegate.onCleared()
+ genericActionDelegate.onCleared()
+ componentEventHandler.onCleared()
+ }
+
+ companion object {
+ private val TAG = LogUtil.getTag()
+
+ @JvmField
+ val PROVIDER = CashAppPayComponentProvider()
+
+ @JvmField
+ val PAYMENT_METHOD_TYPES = listOf(PaymentMethodTypes.CASH_APP_PAY)
+
+ private const val REDIRECT_RESULT_SCHEME = BuildConfig.checkoutRedirectScheme + "://"
+
+ /**
+ * Returns the suggested value to be used as the `returnUrl` value in the /payments call and in the
+ * [CashAppPayConfiguration].
+ *
+ * @param context The context provides the package name which constitutes part of the ReturnUrl
+ * @return The suggested `returnUrl` to be used. Consists of "adyencheckout://" + App package name.
+ */
+ fun getReturnUrl(context: Context): String {
+ return REDIRECT_RESULT_SCHEME + context.packageName
+ }
+ }
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponentState.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponentState.kt
new file mode 100644
index 0000000000..1be77212fb
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayComponentState.kt
@@ -0,0 +1,22 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay
+
+import com.adyen.checkout.components.core.PaymentComponentData
+import com.adyen.checkout.components.core.PaymentComponentState
+import com.adyen.checkout.components.core.paymentmethod.CashAppPayPaymentMethod
+
+/**
+ * Represents the state of [CashAppPayComponent]
+ */
+data class CashAppPayComponentState(
+ override val data: PaymentComponentData,
+ override val isInputValid: Boolean,
+ override val isReady: Boolean,
+) : PaymentComponentState
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt
new file mode 100644
index 0000000000..d2d2534193
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayConfiguration.kt
@@ -0,0 +1,159 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay
+
+import android.content.Context
+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.internal.ButtonConfiguration
+import com.adyen.checkout.components.core.internal.ButtonConfigurationBuilder
+import com.adyen.checkout.components.core.internal.Configuration
+import com.adyen.checkout.core.Environment
+import kotlinx.parcelize.Parcelize
+import java.util.Locale
+
+/**
+ * Configuration class for the [CashAppPayComponent].
+ */
+@Parcelize
+class CashAppPayConfiguration
+@Suppress("LongParameterList")
+private constructor(
+ override val shopperLocale: Locale,
+ override val environment: Environment,
+ override val clientKey: String,
+ override val isAnalyticsEnabled: Boolean?,
+ override val amount: Amount,
+ override val isSubmitButtonVisible: Boolean?,
+ val genericActionConfiguration: GenericActionConfiguration,
+ val cashAppPayEnvironment: CashAppPayEnvironment?,
+ val returnUrl: String?,
+ val showStorePaymentField: Boolean?,
+ val storePaymentMethod: Boolean?,
+) : Configuration, ButtonConfiguration {
+
+ class Builder :
+ ActionHandlingPaymentMethodConfigurationBuilder,
+ ButtonConfigurationBuilder {
+
+ private var isSubmitButtonVisible: Boolean? = null
+ private var cashAppPayEnvironment: CashAppPayEnvironment? = null
+ private var returnUrl: String? = null
+ private var showStorePaymentField: Boolean? = null
+ private var storePaymentMethod: Boolean? = null
+
+ /**
+ * Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale.
+ *
+ * @param context A Context
+ * @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(context: Context, environment: Environment, clientKey: String) : super(
+ context,
+ environment,
+ clientKey
+ )
+
+ /**
+ * Builder with parameters for a [CashAppPayConfiguration].
+ *
+ * @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, environment: Environment, clientKey: String) : super(
+ shopperLocale,
+ environment,
+ clientKey
+ )
+
+ /**
+ * Sets the environment to be used by Cash App Pay.
+ *
+ * If not set, it will match the Adyen environment.
+ *
+ * @param cashAppPayEnvironment The Cash App Pay environment.
+ */
+ fun setCashAppPayEnvironment(cashAppPayEnvironment: CashAppPayEnvironment): Builder {
+ this.cashAppPayEnvironment = cashAppPayEnvironment
+ return this
+ }
+
+ /**
+ *
+ * Sets the required return URL that Cash App Pay will redirect to at the end of the transaction.
+ *
+ * @param returnUrl The Cash App Pay environment.
+ */
+ fun setReturnUrl(returnUrl: String): Builder {
+ this.returnUrl = returnUrl
+ return this
+ }
+
+ /**
+ * Set if the option to store the shopper's account for future payments should be shown as an input field.
+ *
+ * Default is true.
+ *
+ * When using `sessions` show store payment field will be ignored and replaced with the value sent to
+ * `/sessions` call.
+ *
+ * @param showStorePaymentField [Boolean]
+ * @return [CashAppPayConfiguration.Builder]
+ */
+ fun setShowStorePaymentField(showStorePaymentField: Boolean): Builder {
+ this.showStorePaymentField = showStorePaymentField
+ return this
+ }
+
+ /**
+ * Set if the shopper's account should be stored, when the store payment method switch is not presented to the
+ * shopper.
+ *
+ * Only applicable if [showStorePaymentField] is false.
+ *
+ * Default is false.
+ *
+ * @param storePaymentMethod [Boolean]
+ * @return [CashAppPayConfiguration.Builder]
+ */
+ fun setStorePaymentMethod(storePaymentMethod: Boolean): Builder {
+ this.storePaymentMethod = storePaymentMethod
+ return this
+ }
+
+ /**
+ * Sets if submit button will be visible or not.
+ *
+ * Default is true.
+ *
+ * @param isSubmitButtonVisible If submit button should be visible or not.
+ */
+ override fun setSubmitButtonVisible(isSubmitButtonVisible: Boolean): Builder {
+ this.isSubmitButtonVisible = isSubmitButtonVisible
+ return this
+ }
+
+ override fun buildInternal() = CashAppPayConfiguration(
+ isSubmitButtonVisible = isSubmitButtonVisible,
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey,
+ isAnalyticsEnabled = isAnalyticsEnabled,
+ amount = amount,
+ genericActionConfiguration = genericActionConfigurationBuilder.build(),
+ cashAppPayEnvironment = cashAppPayEnvironment,
+ returnUrl = returnUrl,
+ showStorePaymentField = showStorePaymentField,
+ storePaymentMethod = storePaymentMethod,
+ )
+ }
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayEnvironment.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayEnvironment.kt
new file mode 100644
index 0000000000..91dec1a9c3
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/CashAppPayEnvironment.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 oscars on 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay
+
+enum class CashAppPayEnvironment {
+ SANDBOX,
+ PRODUCTION,
+}
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
new file mode 100644
index 0000000000..67008a4b6b
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/provider/CashAppPayComponentProvider.kt
@@ -0,0 +1,381 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.provider
+
+import android.app.Application
+import androidx.annotation.RestrictTo
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
+import androidx.savedstate.SavedStateRegistryOwner
+import app.cash.paykit.core.CashAppPayFactory
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.provider.GenericActionComponentProvider
+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.DefaultCashAppPayDelegate
+import com.adyen.checkout.cashapppay.internal.ui.StoredCashAppPayDelegate
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParamsMapper
+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.DefaultComponentEventHandler
+import com.adyen.checkout.components.core.internal.PaymentObserverRepository
+import com.adyen.checkout.components.core.internal.data.api.AnalyticsMapper
+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.data.model.AnalyticsSource
+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.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.sessions.core.CheckoutSession
+import com.adyen.checkout.sessions.core.SessionComponentCallback
+import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler
+import com.adyen.checkout.sessions.core.internal.SessionInteractor
+import com.adyen.checkout.sessions.core.internal.SessionSavedStateHandleContainer
+import com.adyen.checkout.sessions.core.internal.data.api.SessionRepository
+import com.adyen.checkout.sessions.core.internal.data.api.SessionService
+import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponentProvider
+import com.adyen.checkout.sessions.core.internal.provider.SessionStoredPaymentComponentProvider
+import com.adyen.checkout.sessions.core.internal.ui.model.SessionParamsFactory
+import com.adyen.checkout.ui.core.internal.ui.SubmitHandler
+
+class CashAppPayComponentProvider
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+constructor(
+ overrideComponentParams: ComponentParams? = null,
+ overrideSessionParams: SessionParams? = null,
+) :
+ PaymentComponentProvider<
+ CashAppPayComponent,
+ CashAppPayConfiguration,
+ CashAppPayComponentState,
+ ComponentCallback
+ >,
+ StoredPaymentComponentProvider<
+ CashAppPayComponent,
+ CashAppPayConfiguration,
+ CashAppPayComponentState,
+ ComponentCallback
+ >,
+ SessionPaymentComponentProvider<
+ CashAppPayComponent,
+ CashAppPayConfiguration,
+ CashAppPayComponentState,
+ SessionComponentCallback
+ >,
+ SessionStoredPaymentComponentProvider<
+ CashAppPayComponent,
+ CashAppPayConfiguration,
+ CashAppPayComponentState,
+ SessionComponentCallback
+ > {
+
+ private val componentParamsMapper = CashAppPayComponentParamsMapper(overrideComponentParams, overrideSessionParams)
+
+ override fun get(
+ savedStateRegistryOwner: SavedStateRegistryOwner,
+ viewModelStoreOwner: ViewModelStoreOwner,
+ lifecycleOwner: LifecycleOwner,
+ paymentMethod: PaymentMethod,
+ configuration: CashAppPayConfiguration,
+ application: Application,
+ componentCallback: ComponentCallback,
+ order: Order?,
+ key: String?
+ ): CashAppPayComponent {
+ assertSupported(paymentMethod)
+
+ val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle ->
+ val componentParams = componentParamsMapper.mapToParams(configuration, null, paymentMethod)
+ val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
+ val analyticsService = AnalyticsService(httpClient)
+ val analyticsRepository = DefaultAnalyticsRepository(
+ packageName = application.packageName,
+ locale = componentParams.shopperLocale,
+ source = AnalyticsSource.PaymentComponent(componentParams.isCreatedByDropIn, paymentMethod),
+ analyticsService = analyticsService,
+ analyticsMapper = AnalyticsMapper(),
+ )
+
+ val cashAppPayDelegate = DefaultCashAppPayDelegate(
+ submitHandler = SubmitHandler(savedStateHandle),
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = paymentMethod,
+ order = order,
+ componentParams = componentParams,
+ cashAppPayFactory = CashAppPayFactory,
+ )
+
+ val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate(
+ configuration = configuration.genericActionConfiguration,
+ savedStateHandle = savedStateHandle,
+ application = application,
+ )
+
+ CashAppPayComponent(
+ cashAppPayDelegate = cashAppPayDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate),
+ componentEventHandler = DefaultComponentEventHandler(),
+ )
+ }
+
+ return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, CashAppPayComponent::class.java]
+ .also { component ->
+ component.observe(lifecycleOwner) {
+ component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
+ }
+ }
+ }
+
+ override fun get(
+ savedStateRegistryOwner: SavedStateRegistryOwner,
+ viewModelStoreOwner: ViewModelStoreOwner,
+ lifecycleOwner: LifecycleOwner,
+ storedPaymentMethod: StoredPaymentMethod,
+ configuration: CashAppPayConfiguration,
+ 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 httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
+ val analyticsService = AnalyticsService(httpClient)
+ val analyticsRepository = DefaultAnalyticsRepository(
+ packageName = application.packageName,
+ locale = componentParams.shopperLocale,
+ source = AnalyticsSource.PaymentComponent(componentParams.isCreatedByDropIn, storedPaymentMethod),
+ analyticsService = analyticsService,
+ analyticsMapper = AnalyticsMapper(),
+ )
+
+ val cashAppPayDelegate = StoredCashAppPayDelegate(
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = storedPaymentMethod,
+ order = order,
+ componentParams = componentParams,
+ )
+
+ val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate(
+ configuration = configuration.genericActionConfiguration,
+ savedStateHandle = savedStateHandle,
+ application = application,
+ )
+
+ CashAppPayComponent(
+ cashAppPayDelegate = cashAppPayDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate),
+ componentEventHandler = DefaultComponentEventHandler(),
+ )
+ }
+
+ return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, CashAppPayComponent::class.java]
+ .also { component ->
+ component.observe(lifecycleOwner) {
+ component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
+ }
+ }
+ }
+
+ @Suppress("LongMethod")
+ override fun get(
+ savedStateRegistryOwner: SavedStateRegistryOwner,
+ viewModelStoreOwner: ViewModelStoreOwner,
+ lifecycleOwner: LifecycleOwner,
+ checkoutSession: CheckoutSession,
+ paymentMethod: PaymentMethod,
+ configuration: CashAppPayConfiguration,
+ application: Application,
+ componentCallback: SessionComponentCallback,
+ key: String?
+ ): CashAppPayComponent {
+ assertSupported(paymentMethod)
+
+ val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle ->
+ val componentParams = componentParamsMapper.mapToParams(
+ configuration = configuration,
+ sessionParams = SessionParamsFactory.create(checkoutSession),
+ paymentMethod = paymentMethod,
+ )
+
+ val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
+ val analyticsService = AnalyticsService(httpClient)
+ val analyticsRepository = DefaultAnalyticsRepository(
+ packageName = application.packageName,
+ locale = componentParams.shopperLocale,
+ source = AnalyticsSource.PaymentComponent(componentParams.isCreatedByDropIn, paymentMethod),
+ analyticsService = analyticsService,
+ analyticsMapper = AnalyticsMapper(),
+ )
+
+ val cashAppPayDelegate = DefaultCashAppPayDelegate(
+ submitHandler = SubmitHandler(savedStateHandle),
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = paymentMethod,
+ order = checkoutSession.order,
+ componentParams = componentParams,
+ cashAppPayFactory = CashAppPayFactory,
+ )
+
+ val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate(
+ configuration = configuration.genericActionConfiguration,
+ savedStateHandle = savedStateHandle,
+ application = application,
+ )
+
+ 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
+ )
+
+ val sessionComponentEventHandler = SessionComponentEventHandler(
+ sessionInteractor = sessionInteractor,
+ sessionSavedStateHandleContainer = sessionSavedStateHandleContainer,
+ )
+
+ CashAppPayComponent(
+ cashAppPayDelegate = cashAppPayDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate),
+ componentEventHandler = sessionComponentEventHandler,
+ )
+ }
+
+ return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, CashAppPayComponent::class.java]
+ .also { component ->
+ component.observe(lifecycleOwner) {
+ component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
+ }
+ }
+ }
+
+ override fun get(
+ savedStateRegistryOwner: SavedStateRegistryOwner,
+ viewModelStoreOwner: ViewModelStoreOwner,
+ lifecycleOwner: LifecycleOwner,
+ checkoutSession: CheckoutSession,
+ storedPaymentMethod: StoredPaymentMethod,
+ configuration: CashAppPayConfiguration,
+ application: Application,
+ componentCallback: SessionComponentCallback,
+ key: String?
+ ): CashAppPayComponent {
+ assertSupported(storedPaymentMethod)
+
+ val viewModelFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle ->
+ val componentParams = componentParamsMapper.mapToParams(
+ configuration = configuration,
+ sessionParams = SessionParamsFactory.create(checkoutSession),
+ paymentMethod = storedPaymentMethod,
+ )
+
+ val httpClient = HttpClientFactory.getHttpClient(componentParams.environment)
+ val analyticsService = AnalyticsService(httpClient)
+ val analyticsRepository = DefaultAnalyticsRepository(
+ packageName = application.packageName,
+ locale = componentParams.shopperLocale,
+ source = AnalyticsSource.PaymentComponent(componentParams.isCreatedByDropIn, storedPaymentMethod),
+ analyticsService = analyticsService,
+ analyticsMapper = AnalyticsMapper(),
+ )
+
+ val cashAppPayDelegate = StoredCashAppPayDelegate(
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = storedPaymentMethod,
+ order = checkoutSession.order,
+ componentParams = componentParams,
+ )
+
+ val genericActionDelegate = GenericActionComponentProvider(componentParams).getDelegate(
+ configuration = configuration.genericActionConfiguration,
+ savedStateHandle = savedStateHandle,
+ application = application,
+ )
+
+ 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
+ )
+
+ val sessionComponentEventHandler = SessionComponentEventHandler(
+ sessionInteractor = sessionInteractor,
+ sessionSavedStateHandleContainer = sessionSavedStateHandleContainer,
+ )
+
+ CashAppPayComponent(
+ cashAppPayDelegate = cashAppPayDelegate,
+ genericActionDelegate = genericActionDelegate,
+ actionHandlingComponent = DefaultActionHandlingComponent(genericActionDelegate, cashAppPayDelegate),
+ componentEventHandler = sessionComponentEventHandler,
+ )
+ }
+
+ return ViewModelProvider(viewModelStoreOwner, viewModelFactory)[key, CashAppPayComponent::class.java]
+ .also { component ->
+ component.observe(lifecycleOwner) {
+ component.componentEventHandler.onPaymentComponentEvent(it, componentCallback)
+ }
+ }
+ }
+
+ private fun assertSupported(paymentMethod: PaymentMethod) {
+ if (!isPaymentMethodSupported(paymentMethod)) {
+ throw ComponentException("Unsupported payment method ${paymentMethod.type}")
+ }
+ }
+
+ private fun assertSupported(paymentMethod: StoredPaymentMethod) {
+ if (!isPaymentMethodSupported(paymentMethod)) {
+ throw ComponentException("Unsupported payment method ${paymentMethod.type}")
+ }
+ }
+
+ override fun isPaymentMethodSupported(paymentMethod: PaymentMethod): Boolean {
+ return CashAppPayComponent.PAYMENT_METHOD_TYPES.contains(paymentMethod.type)
+ }
+
+ override fun isPaymentMethodSupported(storedPaymentMethod: StoredPaymentMethod): Boolean {
+ return CashAppPayComponent.PAYMENT_METHOD_TYPES.contains(storedPaymentMethod.type)
+ }
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/CashAppPayDelegate.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/CashAppPayDelegate.kt
new file mode 100644
index 0000000000..bb9c079dc7
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/CashAppPayDelegate.kt
@@ -0,0 +1,24 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui
+
+import com.adyen.checkout.cashapppay.CashAppPayComponentState
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayInputData
+import com.adyen.checkout.components.core.internal.ui.PaymentComponentDelegate
+import com.adyen.checkout.ui.core.internal.ui.ViewProvidingDelegate
+import kotlinx.coroutines.flow.Flow
+
+internal interface CashAppPayDelegate :
+ PaymentComponentDelegate,
+ ViewProvidingDelegate {
+
+ val componentStateFlow: Flow
+
+ fun updateInputData(update: CashAppPayInputData.() -> Unit)
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/CashAppPayViewProvider.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/CashAppPayViewProvider.kt
new file mode 100644
index 0000000000..ff42afd501
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/CashAppPayViewProvider.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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui
+
+import android.content.Context
+import android.util.AttributeSet
+import com.adyen.checkout.cashapppay.internal.ui.view.CashAppPayButtonView
+import com.adyen.checkout.cashapppay.internal.ui.view.CashAppPayView
+import com.adyen.checkout.cashapppay.internal.ui.view.CashAppPayWaitingView
+import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType
+import com.adyen.checkout.ui.core.internal.ui.ButtonViewProvider
+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.PayButton
+
+internal object CashAppPayViewProvider : ViewProvider {
+
+ override fun getView(
+ viewType: ComponentViewType,
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ): ComponentView = when (viewType) {
+ CashAppPayComponentViewType -> CashAppPayView(context, attrs, defStyleAttr)
+ PaymentInProgressViewType -> CashAppPayWaitingView(context, attrs, defStyleAttr)
+ else -> throw IllegalArgumentException("Unsupported view type")
+ }
+}
+
+internal class CashAppPayButtonViewProvider : ButtonViewProvider {
+ override fun getButton(context: Context, attrs: AttributeSet?, defStyleAttr: Int): PayButton =
+ CashAppPayButtonView(context, attrs, defStyleAttr)
+}
+
+internal object CashAppPayComponentViewType : ButtonComponentViewType {
+
+ override val buttonViewProvider: ButtonViewProvider get() = CashAppPayButtonViewProvider()
+
+ override val viewProvider: ViewProvider = CashAppPayViewProvider
+
+ override val buttonTextResId: Int = ButtonComponentViewType.DEFAULT_BUTTON_TEXT_RES_ID
+}
+
+internal object PaymentInProgressViewType : ComponentViewType {
+ override val viewProvider: ViewProvider = CashAppPayViewProvider
+}
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
new file mode 100644
index 0000000000..80e9072030
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegate.kt
@@ -0,0 +1,332 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui
+
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.LifecycleOwner
+import app.cash.paykit.core.CashAppPay
+import app.cash.paykit.core.CashAppPayFactory
+import app.cash.paykit.core.CashAppPayListener
+import app.cash.paykit.core.CashAppPayState
+import app.cash.paykit.core.models.response.CustomerResponseData
+import app.cash.paykit.core.models.response.GrantType
+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.CashAppPayEnvironment
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayAuthorizationData
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParams
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayInputData
+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.CheckoutCurrency
+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.PaymentMethodTypes
+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.components.core.internal.util.isEmpty
+import com.adyen.checkout.components.core.paymentmethod.CashAppPayPaymentMethod
+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.ui.core.internal.ui.ButtonComponentViewType
+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.SubmitHandler
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.launch
+
+@Suppress("TooManyFunctions")
+internal class DefaultCashAppPayDelegate
+@Suppress("LongParameterList")
+constructor(
+ private val submitHandler: SubmitHandler,
+ private val analyticsRepository: AnalyticsRepository,
+ private val observerRepository: PaymentObserverRepository,
+ private val paymentMethod: PaymentMethod,
+ private val order: OrderRequest?,
+ override val componentParams: CashAppPayComponentParams,
+ private val cashAppPayFactory: CashAppPayFactory,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : CashAppPayDelegate, ButtonDelegate, CashAppPayListener {
+
+ private val inputData = CashAppPayInputData()
+
+ private var outputData = createOutputData()
+
+ private val _componentStateFlow = MutableStateFlow(createComponentState())
+ override val componentStateFlow: Flow = _componentStateFlow
+
+ private val _viewFlow: MutableStateFlow = MutableStateFlow(CashAppPayComponentViewType)
+ override val viewFlow: Flow = _viewFlow
+
+ private val exceptionChannel: Channel = bufferedChannel()
+ val exceptionFlow: Flow = exceptionChannel.receiveAsFlow()
+
+ override val submitFlow: Flow = submitHandler.submitFlow
+
+ private var _coroutineScope: CoroutineScope? = null
+ private val coroutineScope: CoroutineScope get() = requireNotNull(_coroutineScope)
+
+ private lateinit var cashAppPay: CashAppPay
+
+ override fun initialize(coroutineScope: CoroutineScope) {
+ _coroutineScope = coroutineScope
+ submitHandler.initialize(coroutineScope, componentStateFlow)
+
+ cashAppPay = initCashAppPay()
+
+ sendAnalyticsEvent(coroutineScope)
+
+ if (!isConfirmationRequired()) {
+ initiatePayment()
+ }
+ }
+
+ private fun initCashAppPay(): CashAppPay {
+ return if (componentParams.cashAppPayEnvironment == CashAppPayEnvironment.SANDBOX) {
+ cashAppPayFactory.createSandbox(componentParams.requireClientId())
+ } else {
+ cashAppPayFactory.create(componentParams.requireClientId())
+ }.apply {
+ registerForStateUpdates(this@DefaultCashAppPayDelegate)
+ }
+ }
+
+ private fun sendAnalyticsEvent(coroutineScope: CoroutineScope) {
+ Logger.v(TAG, "sendAnalyticsEvent")
+ coroutineScope.launch {
+ analyticsRepository.sendAnalyticsEvent()
+ }
+ }
+
+ override fun observe(
+ lifecycleOwner: LifecycleOwner,
+ coroutineScope: CoroutineScope,
+ callback: (PaymentComponentEvent) -> Unit
+ ) {
+ observerRepository.addObservers(
+ stateFlow = componentStateFlow,
+ exceptionFlow = exceptionFlow,
+ submitFlow = submitFlow,
+ lifecycleOwner = lifecycleOwner,
+ coroutineScope = coroutineScope,
+ callback = callback
+ )
+ }
+
+ override fun removeObserver() {
+ observerRepository.removeObservers()
+ }
+
+ override fun updateInputData(update: CashAppPayInputData.() -> Unit) {
+ inputData.update()
+ onInputDataChanged()
+ }
+
+ private fun onInputDataChanged() {
+ outputData = createOutputData()
+ updateComponentState(outputData)
+ }
+
+ private fun createOutputData(): CashAppPayOutputData {
+ return CashAppPayOutputData(
+ isStorePaymentSelected = inputData.isStorePaymentSelected,
+ authorizationData = inputData.authorizationData,
+ )
+ }
+
+ @VisibleForTesting
+ internal fun updateComponentState(outputData: CashAppPayOutputData) {
+ Logger.v(TAG, "updateComponentState")
+ val componentState = createComponentState(outputData)
+ _componentStateFlow.tryEmit(componentState)
+ }
+
+ private fun createComponentState(
+ outputData: CashAppPayOutputData = this.outputData
+ ): CashAppPayComponentState {
+ val oneTimeData = outputData.authorizationData?.oneTimeData
+ val onFileData = outputData.authorizationData?.onFileData
+
+ val cashAppPayPaymentMethod = CashAppPayPaymentMethod(
+ type = paymentMethod.type,
+ grantId = oneTimeData?.grantId,
+ customerId = onFileData?.customerId,
+ onFileGrantId = onFileData?.grantId,
+ cashtag = onFileData?.cashTag,
+ )
+
+ val paymentComponentData = PaymentComponentData(
+ paymentMethod = cashAppPayPaymentMethod,
+ order = order,
+ amount = componentParams.amount.takeUnless { it.isEmpty },
+ storePaymentMethod = onFileData != null,
+ )
+
+ return CashAppPayComponentState(
+ data = paymentComponentData,
+ isInputValid = outputData.isValid,
+ isReady = true
+ )
+ }
+
+ override fun onSubmit() {
+ if (isConfirmationRequired()) {
+ initiatePayment()
+ }
+ }
+
+ private fun initiatePayment() {
+ val actions = listOfNotNull(
+ getOneTimeAction(),
+ getOnFileAction(outputData),
+ )
+
+ if (actions.isEmpty()) {
+ exceptionChannel.trySend(
+ ComponentException(
+ "Cannot launch Cash App Pay, you need to either pass an amount with supported " +
+ "currency or store the shopper account."
+ )
+ )
+ return
+ }
+
+ _viewFlow.tryEmit(PaymentInProgressViewType)
+
+ coroutineScope.launch(ioDispatcher) {
+ cashAppPay.createCustomerRequest(actions, componentParams.returnUrl)
+ }
+ }
+
+ @Suppress("ReturnCount")
+ private fun getOneTimeAction(): CashAppPayPaymentAction.OneTimeAction? {
+ val amount = componentParams.amount
+
+ // We don't create an OneTimeAction for transactions with no amount
+ if (amount.value <= 0) return null
+
+ val cashAppPayCurrency = when (amount.currency) {
+ CheckoutCurrency.USD.name -> CashAppPayCurrency.USD
+ else -> {
+ exceptionChannel.trySend(ComponentException("Unsupported currency: ${amount.currency}"))
+ return null
+ }
+ }
+
+ return CashAppPayPaymentAction.OneTimeAction(
+ amount = amount.value.toInt(),
+ currency = cashAppPayCurrency,
+ scopeId = componentParams.scopeId,
+ )
+ }
+
+ private fun getOnFileAction(
+ outputData: CashAppPayOutputData,
+ ): CashAppPayPaymentAction.OnFileAction? {
+ val shouldStorePaymentMethod = when {
+ // Shopper is presented with store switch and selected it
+ componentParams.showStorePaymentField && outputData.isStorePaymentSelected -> true
+ // Shopper is not presented with store switch and configuration indicates storing the payment method
+ !componentParams.showStorePaymentField && componentParams.storePaymentMethod -> true
+ else -> false
+ }
+
+ // We don't create an OnFileAction when storing is not required
+ if (!shouldStorePaymentMethod) return null
+
+ return CashAppPayPaymentAction.OnFileAction(
+ scopeId = componentParams.scopeId,
+ )
+ }
+
+ override fun cashAppPayStateDidChange(newState: CashAppPayState) {
+ Logger.d(TAG, "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")
+ updateInputData {
+ authorizationData = createAuthorizationData(newState.responseData)
+ }
+ submitHandler.onSubmit(_componentStateFlow.value)
+ }
+
+ CashAppPayState.Declined -> {
+ Logger.i(TAG, "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)
+ )
+ }
+
+ else -> Unit
+ }
+ }
+
+ private fun createAuthorizationData(customerResponseData: CustomerResponseData): CashAppPayAuthorizationData {
+ val grants = customerResponseData.grants.orEmpty()
+ val oneTimeData = grants.find { it.type == GrantType.ONE_TIME }?.let { CashAppPayOneTimeData(it.id) }
+ val onFileData = grants.find { it.type == GrantType.EXTENDED }?.let {
+ CashAppPayOnFileData(
+ grantId = it.id,
+ cashTag = customerResponseData.customerProfile?.cashTag,
+ customerId = customerResponseData.customerProfile?.id
+ )
+ }
+
+ return CashAppPayAuthorizationData(
+ oneTimeData = oneTimeData,
+ onFileData = onFileData,
+ )
+ }
+
+ override fun isConfirmationRequired(): Boolean =
+ _viewFlow.value is ButtonComponentViewType &&
+ componentParams.showStorePaymentField
+
+ override fun shouldShowSubmitButton(): Boolean = isConfirmationRequired() && componentParams.isSubmitButtonVisible
+
+ internal fun setInteractionBlocked(isInteractionBlocked: Boolean) {
+ submitHandler.setInteractionBlocked(isInteractionBlocked)
+ }
+
+ override fun getPaymentMethodType(): String {
+ return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN
+ }
+
+ override fun onCleared() {
+ _coroutineScope = null
+ 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
new file mode 100644
index 0000000000..a7646349d5
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegate.kt
@@ -0,0 +1,129 @@
+/*
+ * 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 27/6/2023.
+ */
+
+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
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayInputData
+import com.adyen.checkout.components.core.OrderRequest
+import com.adyen.checkout.components.core.PaymentComponentData
+import com.adyen.checkout.components.core.PaymentMethodTypes
+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.util.bufferedChannel
+import com.adyen.checkout.components.core.internal.util.isEmpty
+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.ui.core.internal.ui.ComponentViewType
+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 kotlinx.coroutines.launch
+
+@Suppress("TooManyFunctions")
+internal class StoredCashAppPayDelegate(
+ private val analyticsRepository: AnalyticsRepository,
+ private val observerRepository: PaymentObserverRepository,
+ private val paymentMethod: StoredPaymentMethod,
+ private val order: OrderRequest?,
+ override val componentParams: CashAppPayComponentParams,
+) : CashAppPayDelegate {
+
+ private val _componentStateFlow = MutableStateFlow(createComponentState())
+ override val componentStateFlow: Flow = _componentStateFlow
+
+ private val _viewFlow: MutableStateFlow = MutableStateFlow(null)
+ override val viewFlow: Flow = _viewFlow
+
+ private val submitChannel = bufferedChannel()
+ override val submitFlow: Flow = submitChannel.receiveAsFlow()
+
+ override fun initialize(coroutineScope: CoroutineScope) {
+ sendAnalyticsEvent(coroutineScope)
+
+ componentStateFlow.onEach {
+ onState(it)
+ }.launchIn(coroutineScope)
+ }
+
+ private fun sendAnalyticsEvent(coroutineScope: CoroutineScope) {
+ Logger.v(TAG, "sendAnalyticsEvent")
+ coroutineScope.launch {
+ analyticsRepository.sendAnalyticsEvent()
+ }
+ }
+
+ private fun onState(componentState: CashAppPayComponentState) {
+ if (componentState.isValid) {
+ submitChannel.trySend(componentState)
+ }
+ }
+
+ override fun observe(
+ lifecycleOwner: LifecycleOwner,
+ coroutineScope: CoroutineScope,
+ callback: (PaymentComponentEvent) -> Unit
+ ) {
+ observerRepository.addObservers(
+ stateFlow = componentStateFlow,
+ exceptionFlow = null,
+ submitFlow = submitFlow,
+ lifecycleOwner = lifecycleOwner,
+ coroutineScope = coroutineScope,
+ callback = callback
+ )
+ }
+
+ override fun removeObserver() {
+ observerRepository.removeObservers()
+ }
+
+ override fun updateInputData(update: CashAppPayInputData.() -> Unit) {
+ Log.w(TAG, "updateInputData should not be called for stored Cash App Pay")
+ }
+
+ private fun createComponentState(): CashAppPayComponentState {
+ val cashAppPayPaymentMethod = CashAppPayPaymentMethod(
+ type = paymentMethod.type,
+ storedPaymentMethodId = paymentMethod.id,
+ )
+
+ val paymentComponentData = PaymentComponentData(
+ paymentMethod = cashAppPayPaymentMethod,
+ order = order,
+ amount = componentParams.amount.takeUnless { it.isEmpty },
+ )
+
+ return CashAppPayComponentState(
+ data = paymentComponentData,
+ isInputValid = true,
+ isReady = true
+ )
+ }
+
+ override fun getPaymentMethodType(): String {
+ return paymentMethod.type ?: PaymentMethodTypes.UNKNOWN
+ }
+
+ 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
new file mode 100644
index 0000000000..118784393e
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParams.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 26/6/2023.
+ */
+
+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.ButtonParams
+import com.adyen.checkout.components.core.internal.ui.model.ComponentParams
+import com.adyen.checkout.core.Environment
+import kotlinx.parcelize.Parcelize
+import java.util.Locale
+
+@Parcelize
+internal data class CashAppPayComponentParams(
+ override val isSubmitButtonVisible: Boolean,
+ override val shopperLocale: Locale,
+ override val environment: Environment,
+ override val clientKey: String,
+ override val isAnalyticsEnabled: Boolean,
+ 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 {
+
+ 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
new file mode 100644
index 0000000000..c8aa8ad2e9
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapper.kt
@@ -0,0 +1,113 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui.model
+
+import com.adyen.checkout.cashapppay.CashAppPayConfiguration
+import com.adyen.checkout.cashapppay.CashAppPayEnvironment
+import com.adyen.checkout.components.core.PaymentMethod
+import com.adyen.checkout.components.core.StoredPaymentMethod
+import com.adyen.checkout.components.core.internal.ui.model.ComponentParams
+import com.adyen.checkout.components.core.internal.ui.model.SessionParams
+import com.adyen.checkout.core.Environment
+import com.adyen.checkout.core.exception.ComponentException
+
+internal class CashAppPayComponentParamsMapper(
+ private val overrideComponentParams: ComponentParams?,
+ private val overrideSessionParams: SessionParams?,
+) {
+
+ @Suppress("ThrowsCount")
+ fun mapToParams(
+ configuration: CashAppPayConfiguration,
+ sessionParams: SessionParams?,
+ paymentMethod: PaymentMethod,
+ ): 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)
+
+ if (params.returnUrl == null) {
+ throw ComponentException(
+ "Cannot launch Cash App Pay, set the returnUrl in your CashAppPayConfiguration.Builder"
+ )
+ }
+
+ return params
+ }
+
+ fun mapToParams(
+ configuration: CashAppPayConfiguration,
+ 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(
+ clientId: String?,
+ scopeId: String?,
+ ) = CashAppPayComponentParams(
+ isSubmitButtonVisible = isSubmitButtonVisible ?: true,
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey,
+ isAnalyticsEnabled = isAnalyticsEnabled ?: true,
+ isCreatedByDropIn = false,
+ amount = amount,
+ cashAppPayEnvironment = getCashAppPayEnvironment(),
+ returnUrl = returnUrl,
+ showStorePaymentField = showStorePaymentField ?: true,
+ storePaymentMethod = storePaymentMethod ?: false,
+ clientId = clientId,
+ scopeId = scopeId,
+ )
+
+ private fun CashAppPayConfiguration.getCashAppPayEnvironment(): CashAppPayEnvironment {
+ return when {
+ cashAppPayEnvironment != null -> 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,
+ isAnalyticsEnabled = overrideComponentParams.isAnalyticsEnabled,
+ isCreatedByDropIn = overrideComponentParams.isCreatedByDropIn,
+ amount = overrideComponentParams.amount,
+ )
+ }
+
+ private fun CashAppPayComponentParams.override(
+ sessionParams: SessionParams?,
+ ): CashAppPayComponentParams {
+ if (sessionParams == null) return this
+ return copy(
+ amount = sessionParams.amount ?: amount,
+ showStorePaymentField = sessionParams.enableStoreDetails ?: showStorePaymentField,
+ returnUrl = sessionParams.returnUrl ?: returnUrl
+ )
+ }
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayInputData.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayInputData.kt
new file mode 100644
index 0000000000..c42eec57f3
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayInputData.kt
@@ -0,0 +1,16 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui.model
+
+import com.adyen.checkout.components.core.internal.ui.model.InputData
+
+internal data class CashAppPayInputData(
+ var isStorePaymentSelected: Boolean = false,
+ var authorizationData: CashAppPayAuthorizationData? = null,
+) : InputData
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayOutputData.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayOutputData.kt
new file mode 100644
index 0000000000..b8fe8a9fad
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayOutputData.kt
@@ -0,0 +1,35 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui.model
+
+import com.adyen.checkout.components.core.internal.ui.model.OutputData
+
+internal data class CashAppPayOutputData(
+ val isStorePaymentSelected: Boolean,
+ val authorizationData: CashAppPayAuthorizationData?,
+) : OutputData {
+
+ override val isValid: Boolean
+ get() = authorizationData != null
+}
+
+internal data class CashAppPayAuthorizationData(
+ val oneTimeData: CashAppPayOneTimeData?,
+ val onFileData: CashAppPayOnFileData?,
+)
+
+internal data class CashAppPayOneTimeData(
+ val grantId: String?,
+)
+
+internal data class CashAppPayOnFileData(
+ val grantId: String?,
+ val cashTag: String?,
+ val customerId: String?,
+)
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayButtonView.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayButtonView.kt
new file mode 100644
index 0000000000..77ae87dc89
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayButtonView.kt
@@ -0,0 +1,30 @@
+/*
+ * 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 30/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import com.adyen.checkout.cashapppay.databinding.CashAppPayButtonViewBinding
+import com.adyen.checkout.ui.core.internal.ui.view.PayButton
+
+internal class CashAppPayButtonView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : PayButton(context, attrs, defStyleAttr) {
+
+ private val binding = CashAppPayButtonViewBinding.inflate(LayoutInflater.from(context), this)
+
+ override fun setOnClickListener(listener: OnClickListener?) {
+ binding.payButton.setOnClickListener(listener)
+ }
+
+ override fun setText(text: String?) = Unit
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayView.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayView.kt
new file mode 100644
index 0000000000..d940271ef6
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayView.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 26/6/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.isVisible
+import com.adyen.checkout.cashapppay.R
+import com.adyen.checkout.cashapppay.databinding.CashAppPayViewBinding
+import com.adyen.checkout.cashapppay.internal.ui.CashAppPayDelegate
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParams
+import com.adyen.checkout.components.core.internal.ui.ComponentDelegate
+import com.adyen.checkout.ui.core.internal.ui.ComponentView
+import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle
+import kotlinx.coroutines.CoroutineScope
+
+internal class CashAppPayView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : LinearLayout(context, attrs, defStyleAttr), ComponentView {
+
+ private val binding = CashAppPayViewBinding.inflate(LayoutInflater.from(context), this)
+
+ private lateinit var delegate: CashAppPayDelegate
+
+ init {
+ orientation = VERTICAL
+
+ val padding = resources.getDimension(R.dimen.standard_margin).toInt()
+ setPadding(padding, padding, padding, 0)
+ }
+
+ override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
+ require(delegate is CashAppPayDelegate) { "Unsupported delegate type" }
+ this.delegate = delegate
+
+ initLocalizedStrings(localizedContext)
+ initSwitch()
+ }
+
+ private fun initLocalizedStrings(localizedContext: Context) {
+ binding.switchStorePaymentMethod.setLocalizedTextFromStyle(
+ R.style.AdyenCheckout_CashAppPay_StorePaymentSwitch,
+ localizedContext
+ )
+ }
+
+ private fun initSwitch() {
+ binding.switchStorePaymentMethod.isVisible =
+ (delegate.componentParams as CashAppPayComponentParams).showStorePaymentField
+ binding.switchStorePaymentMethod.setOnCheckedChangeListener { _, isChecked ->
+ delegate.updateInputData { isStorePaymentSelected = isChecked }
+ }
+ }
+
+ override fun highlightValidationErrors() = Unit
+
+ override fun getView(): View = this
+}
diff --git a/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayWaitingView.kt b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayWaitingView.kt
new file mode 100644
index 0000000000..d44c0bc130
--- /dev/null
+++ b/cashapppay/src/main/java/com/adyen/checkout/cashapppay/internal/ui/view/CashAppPayWaitingView.kt
@@ -0,0 +1,46 @@
+package com.adyen.checkout.cashapppay.internal.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import com.adyen.checkout.cashapppay.R
+import com.adyen.checkout.cashapppay.databinding.CashAppPayWaitingViewBinding
+import com.adyen.checkout.components.core.internal.ui.ComponentDelegate
+import com.adyen.checkout.ui.core.internal.ui.ComponentView
+import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle
+import kotlinx.coroutines.CoroutineScope
+
+internal class CashAppPayWaitingView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : LinearLayout(context, attrs, defStyleAttr), ComponentView {
+
+ private val binding = CashAppPayWaitingViewBinding.inflate(LayoutInflater.from(context), this)
+
+ init {
+ orientation = HORIZONTAL
+ gravity = Gravity.CENTER
+
+ val padding = resources.getDimension(R.dimen.standard_margin).toInt()
+ setPadding(padding, padding, padding, padding)
+ }
+
+ override fun initView(delegate: ComponentDelegate, coroutineScope: CoroutineScope, localizedContext: Context) {
+ initLocalizedStrings(localizedContext)
+ }
+
+ private fun initLocalizedStrings(localizedContext: Context) {
+ binding.textViewPaymentInProgressDescription.setLocalizedTextFromStyle(
+ R.style.AdyenCheckout_CashAppPay_WaitingDescriptionTextView,
+ localizedContext
+ )
+ }
+
+ override fun highlightValidationErrors() = Unit
+
+ override fun getView(): View = this
+}
diff --git a/cashapppay/src/main/res/layout-night/cash_app_pay_button_view.xml b/cashapppay/src/main/res/layout-night/cash_app_pay_button_view.xml
new file mode 100644
index 0000000000..08061bf235
--- /dev/null
+++ b/cashapppay/src/main/res/layout-night/cash_app_pay_button_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/cashapppay/src/main/res/layout/cash_app_pay_button_view.xml b/cashapppay/src/main/res/layout/cash_app_pay_button_view.xml
new file mode 100644
index 0000000000..d1a8f65f82
--- /dev/null
+++ b/cashapppay/src/main/res/layout/cash_app_pay_button_view.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/cashapppay/src/main/res/layout/cash_app_pay_view.xml b/cashapppay/src/main/res/layout/cash_app_pay_view.xml
new file mode 100644
index 0000000000..482279a9f2
--- /dev/null
+++ b/cashapppay/src/main/res/layout/cash_app_pay_view.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/cashapppay/src/main/res/layout/cash_app_pay_waiting_view.xml b/cashapppay/src/main/res/layout/cash_app_pay_waiting_view.xml
new file mode 100644
index 0000000000..d002b61f8b
--- /dev/null
+++ b/cashapppay/src/main/res/layout/cash_app_pay_waiting_view.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/cashapppay/src/main/res/template/values/strings.xml.tt b/cashapppay/src/main/res/template/values/strings.xml.tt
new file mode 100644
index 0000000000..3ce489f054
--- /dev/null
+++ b/cashapppay/src/main/res/template/values/strings.xml.tt
@@ -0,0 +1,12 @@
+
+
+
+ %%storeDetails%%
+ %%paypal.processingPayment%%
+
diff --git a/cashapppay/src/main/res/values-ar/strings.xml b/cashapppay/src/main/res/values-ar/strings.xml
new file mode 100644
index 0000000000..35c2dc9229
--- /dev/null
+++ b/cashapppay/src/main/res/values-ar/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ حفظ لمدفوعاتي القادمة
+ جارِ معالجة المدفوعات…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-cs-rCZ/strings.xml b/cashapppay/src/main/res/values-cs-rCZ/strings.xml
new file mode 100644
index 0000000000..f3130b8b13
--- /dev/null
+++ b/cashapppay/src/main/res/values-cs-rCZ/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Uložit pro příští platby
+ Zpracování platby…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-da-rDK/strings.xml b/cashapppay/src/main/res/values-da-rDK/strings.xml
new file mode 100644
index 0000000000..9a04868b46
--- /dev/null
+++ b/cashapppay/src/main/res/values-da-rDK/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Gem til min næste betaling
+ Behandler betaling…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-de-rDE/strings.xml b/cashapppay/src/main/res/values-de-rDE/strings.xml
new file mode 100644
index 0000000000..6da99f1441
--- /dev/null
+++ b/cashapppay/src/main/res/values-de-rDE/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Für zukünftige Zahlvorgänge speichern
+ Zahlung wird verarbeitet…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-el-rGR/strings.xml b/cashapppay/src/main/res/values-el-rGR/strings.xml
new file mode 100644
index 0000000000..d1093bb8a1
--- /dev/null
+++ b/cashapppay/src/main/res/values-el-rGR/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Αποθήκευση για την επόμενη πληρωμή μου
+ Επεξεργασία πληρωμής…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-es-rES/strings.xml b/cashapppay/src/main/res/values-es-rES/strings.xml
new file mode 100644
index 0000000000..fb65b4cfb5
--- /dev/null
+++ b/cashapppay/src/main/res/values-es-rES/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Recordar para mi próximo pago
+ Procesando pago…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-fi-rFI/strings.xml b/cashapppay/src/main/res/values-fi-rFI/strings.xml
new file mode 100644
index 0000000000..2c258e26c2
--- /dev/null
+++ b/cashapppay/src/main/res/values-fi-rFI/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Tallenna seuraavaa maksuani varten
+ Maksua käsitellään…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-fr-rFR/strings.xml b/cashapppay/src/main/res/values-fr-rFR/strings.xml
new file mode 100644
index 0000000000..a5fb32d0da
--- /dev/null
+++ b/cashapppay/src/main/res/values-fr-rFR/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Sauvegarder pour mon prochain paiement
+ Traitement du paiement en cours…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-hr-rHR/strings.xml b/cashapppay/src/main/res/values-hr-rHR/strings.xml
new file mode 100644
index 0000000000..074bdea346
--- /dev/null
+++ b/cashapppay/src/main/res/values-hr-rHR/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Pohrani za moje sljedeće plaćanje
+ Obrada plaćanja u tijeku…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-hu-rHU/strings.xml b/cashapppay/src/main/res/values-hu-rHU/strings.xml
new file mode 100644
index 0000000000..f9d33d06ea
--- /dev/null
+++ b/cashapppay/src/main/res/values-hu-rHU/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Mentés a következő fizetéshez
+ Fizetés feldolgozása…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-it-rIT/strings.xml b/cashapppay/src/main/res/values-it-rIT/strings.xml
new file mode 100644
index 0000000000..c65e637eb8
--- /dev/null
+++ b/cashapppay/src/main/res/values-it-rIT/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Salva per il prossimo pagamento
+ Elaborazione del pagamento in corso…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-ja-rJP/strings.xml b/cashapppay/src/main/res/values-ja-rJP/strings.xml
new file mode 100644
index 0000000000..c9d4f518c7
--- /dev/null
+++ b/cashapppay/src/main/res/values-ja-rJP/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ 次回のお支払いのため詳細を保存
+ 支払いを処理しています…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-ko-rKR/strings.xml b/cashapppay/src/main/res/values-ko-rKR/strings.xml
new file mode 100644
index 0000000000..9a76ef3a17
--- /dev/null
+++ b/cashapppay/src/main/res/values-ko-rKR/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ 다음 결제를 위해 이 수단 저장
+ 결제 처리 중…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-nb-rNO/strings.xml b/cashapppay/src/main/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000000..44ec4ade72
--- /dev/null
+++ b/cashapppay/src/main/res/values-nb-rNO/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Lagre til min neste betaling
+ Behandler betaling…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-nl-rNL/strings.xml b/cashapppay/src/main/res/values-nl-rNL/strings.xml
new file mode 100644
index 0000000000..438e40b0f2
--- /dev/null
+++ b/cashapppay/src/main/res/values-nl-rNL/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Bewaar voor mijn volgende betaling
+ Betaling wordt verwerkt…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-pl-rPL/strings.xml b/cashapppay/src/main/res/values-pl-rPL/strings.xml
new file mode 100644
index 0000000000..1077bfa1b7
--- /dev/null
+++ b/cashapppay/src/main/res/values-pl-rPL/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Zapisz na potrzeby następnej płatności
+ Przetwarzanie płatności…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-pt-rBR/strings.xml b/cashapppay/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000000..1cacb39e15
--- /dev/null
+++ b/cashapppay/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Salvar para meu próximo pagamento
+ Processando pagamento…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-pt-rPT/strings.xml b/cashapppay/src/main/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000000..5d3b9bfb9b
--- /dev/null
+++ b/cashapppay/src/main/res/values-pt-rPT/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Guardar para o meu próximo pagamento
+ A processar pagamento…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-ro-rRO/strings.xml b/cashapppay/src/main/res/values-ro-rRO/strings.xml
new file mode 100644
index 0000000000..b3c7be1122
--- /dev/null
+++ b/cashapppay/src/main/res/values-ro-rRO/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Salvează pentru următoarea mea plată
+ Se prelucrează plata…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-ru-rRU/strings.xml b/cashapppay/src/main/res/values-ru-rRU/strings.xml
new file mode 100644
index 0000000000..cddba7941e
--- /dev/null
+++ b/cashapppay/src/main/res/values-ru-rRU/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Сохранить для следующего платежа
+ Платеж обрабатывается…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-sk-rSK/strings.xml b/cashapppay/src/main/res/values-sk-rSK/strings.xml
new file mode 100644
index 0000000000..d01fcdffc6
--- /dev/null
+++ b/cashapppay/src/main/res/values-sk-rSK/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Uložiť pre moju ďalšiu platbu
+ Platba sa spracúva.
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-sl-rSI/strings.xml b/cashapppay/src/main/res/values-sl-rSI/strings.xml
new file mode 100644
index 0000000000..b8fd33aea6
--- /dev/null
+++ b/cashapppay/src/main/res/values-sl-rSI/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Shrani za moje naslednje plačilo
+ Obdelava plačila…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-sv-rSE/strings.xml b/cashapppay/src/main/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000000..f3d171a9ee
--- /dev/null
+++ b/cashapppay/src/main/res/values-sv-rSE/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Spara till min nästa betalning
+ Behandlar betalning…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-zh-rCN/strings.xml b/cashapppay/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000000..7dcf79fd29
--- /dev/null
+++ b/cashapppay/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ 保存以便下次支付使用
+ 正在处理付款…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values-zh-rTW/strings.xml b/cashapppay/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000000..be93cf78a9
--- /dev/null
+++ b/cashapppay/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ 儲存以供下次付款使用
+ 正在處理付款……
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values/strings.xml b/cashapppay/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..afdcb4ea2b
--- /dev/null
+++ b/cashapppay/src/main/res/values/strings.xml
@@ -0,0 +1,12 @@
+
+
+
+ Save for my next payment
+ Processing payment…
+
\ No newline at end of file
diff --git a/cashapppay/src/main/res/values/styles.xml b/cashapppay/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..ba1cf4c3a9
--- /dev/null
+++ b/cashapppay/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt
new file mode 100644
index 0000000000..d3b808c46a
--- /dev/null
+++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/CashAppPayComponentTest.kt
@@ -0,0 +1,219 @@
+/*
+ * 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 4/7/2023.
+ */
+
+package com.adyen.checkout.cashapppay
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.viewModelScope
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
+import com.adyen.checkout.cashapppay.internal.ui.CashAppPayComponentViewType
+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.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.TestDispatcherExtension
+import com.adyen.checkout.test.extensions.invokeOnCleared
+import com.adyen.checkout.test.extensions.test
+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
+import org.junit.jupiter.api.Assertions.assertFalse
+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.any
+import org.mockito.kotlin.doReturn
+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
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class)
+internal class CashAppPayComponentTest(
+ @Mock private val cashAppPayDelegate: CashAppPayDelegate,
+ @Mock private val genericActionDelegate: GenericActionDelegate,
+ @Mock private val actionHandlingComponent: DefaultActionHandlingComponent,
+ @Mock private val componentEventHandler: ComponentEventHandler,
+) {
+
+ private lateinit var component: CashAppPayComponent
+
+ @BeforeEach
+ fun before() {
+ whenever(cashAppPayDelegate.viewFlow) doReturn MutableStateFlow(CashAppPayComponentViewType)
+ whenever(genericActionDelegate.viewFlow) doReturn MutableStateFlow(null)
+
+ component = CashAppPayComponent(
+ cashAppPayDelegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+
+ AdyenLogger.setLogLevel(Logger.NONE)
+ }
+
+ @Test
+ fun `when component is created then delegates are initialized`() {
+ verify(cashAppPayDelegate).initialize(component.viewModelScope)
+ verify(genericActionDelegate).initialize(component.viewModelScope)
+ verify(componentEventHandler).initialize(component.viewModelScope)
+ }
+
+ @Test
+ fun `when component is cleared then delegates are cleared`() {
+ component.invokeOnCleared()
+
+ verify(cashAppPayDelegate).onCleared()
+ verify(genericActionDelegate).onCleared()
+ verify(componentEventHandler).onCleared()
+ }
+
+ @Test
+ fun `when observe is called then observe in delegates is called`() {
+ val lifecycleOwner = mock()
+ val callback: (PaymentComponentEvent) -> Unit = {}
+
+ component.observe(lifecycleOwner, callback)
+
+ verify(cashAppPayDelegate).observe(lifecycleOwner, component.viewModelScope, callback)
+ verify(genericActionDelegate).observe(eq(lifecycleOwner), eq(component.viewModelScope), any())
+ }
+
+ @Test
+ fun `when removeObserver is called then removeObserver in delegates is called`() {
+ component.removeObserver()
+
+ verify(cashAppPayDelegate).removeObserver()
+ verify(genericActionDelegate).removeObserver()
+ }
+
+ @Test
+ fun `when component is initialized then view flow should match cash app pay delegate view flow`() = runTest {
+ val testViewFlow = component.viewFlow.test(testScheduler)
+ assertEquals(CashAppPayComponentViewType, testViewFlow.latestValue)
+ }
+
+ @Test
+ fun `when cash app pay delegate view flow emits a value then component view flow should match that value`() =
+ runTest {
+ val delegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1)
+ whenever(cashAppPayDelegate.viewFlow) doReturn delegateViewFlow
+ component = CashAppPayComponent(
+ cashAppPayDelegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+
+ val testViewFlow = component.viewFlow.test(testScheduler)
+
+ assertEquals(TestComponentViewType.VIEW_TYPE_1, testViewFlow.latestValue)
+
+ delegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
+
+ assertEquals(TestComponentViewType.VIEW_TYPE_2, testViewFlow.latestValue)
+ }
+
+ @Test
+ fun `when action delegate view flow emits a value then component view flow should match that value`() = runTest {
+ val actionDelegateViewFlow = MutableStateFlow(TestComponentViewType.VIEW_TYPE_1)
+ whenever(genericActionDelegate.viewFlow) doReturn actionDelegateViewFlow
+ component = CashAppPayComponent(
+ cashAppPayDelegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+
+ val testViewFlow = component.viewFlow.test(testScheduler)
+ // this value should match the value of the main delegate and not the action delegate
+ // and in practice the initial value of the action delegate view flow is always null so it should be ignored
+ assertEquals(CashAppPayComponentViewType, testViewFlow.latestValue)
+
+ actionDelegateViewFlow.emit(TestComponentViewType.VIEW_TYPE_2)
+
+ assertEquals(TestComponentViewType.VIEW_TYPE_2, testViewFlow.latestValue)
+ }
+
+ @Test
+ fun `when isConfirmationRequired and delegate is default, then delegate is called`() {
+ val delegate = mock()
+ whenever(delegate.viewFlow) doReturn MutableStateFlow(CashAppPayComponentViewType)
+ component = CashAppPayComponent(
+ delegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+
+ component.isConfirmationRequired()
+
+ verify(delegate).isConfirmationRequired()
+ }
+
+ @Test
+ fun `when isConfirmationRequired and delegate is stored, then result is false`() {
+ val delegate = mock()
+ whenever(delegate.viewFlow) doReturn MutableStateFlow(CashAppPayComponentViewType)
+ component = CashAppPayComponent(
+ delegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+
+ val result = component.isConfirmationRequired()
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `when submit is called and active delegate is the payment delegate, then delegate onSubmit is called`() {
+ val delegate = mock()
+ whenever(delegate.viewFlow) doReturn MutableStateFlow(CashAppPayComponentViewType)
+ component = CashAppPayComponent(
+ delegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+ whenever(component.delegate).thenReturn(delegate)
+
+ component.submit()
+
+ verify(delegate).onSubmit()
+ }
+
+ @Test
+ fun `when submit is called and active delegate is the action delegate, then delegate onSubmit is not called`() {
+ val delegate = mock()
+ whenever(delegate.viewFlow) doReturn MutableStateFlow(CashAppPayComponentViewType)
+ component = CashAppPayComponent(
+ delegate,
+ genericActionDelegate,
+ actionHandlingComponent,
+ componentEventHandler,
+ )
+ whenever(component.delegate).thenReturn(genericActionDelegate)
+
+ component.submit()
+
+ verify(delegate, never()).onSubmit()
+ }
+}
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
new file mode 100644
index 0000000000..69593615d6
--- /dev/null
+++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/DefaultCashAppPayDelegateTest.kt
@@ -0,0 +1,499 @@
+/*
+ * 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/7/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui
+
+import app.cash.paykit.core.CashAppPay
+import app.cash.paykit.core.CashAppPayFactory
+import app.cash.paykit.core.CashAppPayState
+import app.cash.paykit.core.models.common.Action
+import app.cash.paykit.core.models.response.CustomerProfile
+import app.cash.paykit.core.models.response.CustomerResponseData
+import app.cash.paykit.core.models.response.Grant
+import app.cash.paykit.core.models.response.GrantType
+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.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.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.paymentmethod.CashAppPayPaymentMethod
+import com.adyen.checkout.core.Environment
+import com.adyen.checkout.core.exception.ComponentException
+import com.adyen.checkout.test.TestDispatcherExtension
+import com.adyen.checkout.test.extensions.test
+import com.adyen.checkout.ui.core.internal.ui.SubmitHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+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.BeforeEach
+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.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.anyOrNull
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import java.util.Locale
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(MockitoExtension::class, TestDispatcherExtension::class)
+internal class DefaultCashAppPayDelegateTest(
+ @Mock private val submitHandler: SubmitHandler,
+ @Mock private val analyticsRepository: AnalyticsRepository,
+ @Mock private val cashAppPayFactory: CashAppPayFactory,
+ @Mock private val cashAppPay: CashAppPay,
+) {
+
+ private lateinit var delegate: DefaultCashAppPayDelegate
+
+ @BeforeEach
+ fun before() {
+ whenever(cashAppPayFactory.createSandbox(any())) doReturn cashAppPay
+ whenever(cashAppPayFactory.create(any())) doReturn cashAppPay
+ delegate = createDefaultCashAppPayDelegate()
+ }
+
+ @Nested
+ @DisplayName("when delegate is initialized")
+ inner class InitializeTest {
+
+ @Test
+ fun `then analytics event is sent`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ verify(analyticsRepository).sendAnalyticsEvent()
+ }
+
+ @Test
+ fun `no confirmation is required, then payment should be initiated`() = runTest {
+ delegate = createDefaultCashAppPayDelegate(
+ getConfigurationBuilder()
+ .setAmount(Amount("USD", 10L))
+ .setShowStorePaymentField(false)
+ .build()
+ )
+ delegate.initialize(this)
+
+ verify(cashAppPay).createCustomerRequest(paymentActions = any(), redirectUri = anyOrNull())
+ }
+ }
+
+ @Test
+ fun `when input data changes, then component state is created`() = runTest {
+ delegate = createDefaultCashAppPayDelegate(
+ getConfigurationBuilder()
+ .setAmount(Amount("USD", 10L))
+ .build()
+ )
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.updateInputData {
+ isStorePaymentSelected = true
+ authorizationData = CashAppPayAuthorizationData(
+ oneTimeData = CashAppPayOneTimeData("grantId"),
+ onFileData = CashAppPayOnFileData("grantId", "cashTag", "customerId")
+ )
+ }
+
+ val expected = CashAppPayComponentState(
+ data = PaymentComponentData(
+ paymentMethod = CashAppPayPaymentMethod(
+ type = null,
+ grantId = "grantId",
+ onFileGrantId = "grantId",
+ customerId = "customerId",
+ cashtag = "cashTag",
+ storedPaymentMethodId = null,
+ ),
+ order = TEST_ORDER,
+ amount = Amount("USD", 10L),
+ storePaymentMethod = true,
+ ),
+ isInputValid = true,
+ isReady = true
+ )
+ assertEquals(expected, testFlow.latestValue)
+ }
+
+ @Nested
+ @DisplayName("when submit button is configured to be")
+ inner class SubmitButtonVisibilityTest {
+
+ @Test
+ fun `hidden, then it should not show`() {
+ delegate = createDefaultCashAppPayDelegate(
+ configuration = getConfigurationBuilder()
+ .setSubmitButtonVisible(false)
+ .build()
+ )
+
+ assertFalse(delegate.shouldShowSubmitButton())
+ }
+
+ @Test
+ fun `visible, then it should show`() {
+ delegate = createDefaultCashAppPayDelegate(
+ configuration = getConfigurationBuilder()
+ .setSubmitButtonVisible(true)
+ .build()
+ )
+
+ assertTrue(delegate.shouldShowSubmitButton())
+ }
+ }
+
+ @Nested
+ inner class SubmitHandlerTest {
+
+ @Test
+ fun `when delegate is initialized, then submit handler event is initialized`() = runTest {
+ val coroutineScope = CoroutineScope(UnconfinedTestDispatcher())
+ delegate.initialize(coroutineScope)
+ verify(submitHandler).initialize(coroutineScope, delegate.componentStateFlow)
+ }
+
+ @Test
+ fun `when delegate setInteractionBlocked is called, then submit handler setInteractionBlocked is called`() =
+ runTest {
+ delegate.setInteractionBlocked(true)
+ verify(submitHandler).setInteractionBlocked(true)
+ }
+ }
+
+ @Nested
+ @DisplayName("when onSubmit is called and")
+ inner class OnSubmitTest {
+
+ @Test
+ fun `there are no actions, then an exception should be propagated`() =
+ runTest {
+ val testFlow = delegate.exceptionFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.onSubmit()
+
+ assertTrue(testFlow.latestValue is ComponentException)
+ assertEquals(1, testFlow.values.size)
+ }
+
+ @Test
+ fun `the currency is not supported, then an exception should be propagated`() =
+ runTest {
+ delegate = createDefaultCashAppPayDelegate(
+ getConfigurationBuilder().setAmount(Amount("EUR", 100L)).build()
+ )
+ val testFlow = delegate.exceptionFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.onSubmit()
+
+ assertEquals(2, testFlow.values.size)
+ testFlow.values.forEach {
+ assertTrue(it is ComponentException)
+ }
+ }
+
+ @Test
+ fun `there is any valid action, then the loading view should be shown`() =
+ runTest {
+ delegate = createDefaultCashAppPayDelegate(
+ getConfigurationBuilder().setAmount(Amount("USD", 100L)).build()
+ )
+ val testFlow = delegate.viewFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.onSubmit()
+
+ assertTrue(testFlow.latestValue is PaymentInProgressViewType)
+ }
+
+ @Test
+ fun `there is an OneTimeAction, then the Cash App SDK should be called with it`() =
+ runTest {
+ delegate = createDefaultCashAppPayDelegate(
+ getConfigurationBuilder().setAmount(Amount("USD", 100L)).build()
+ )
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.onSubmit()
+
+ verify(cashAppPay).createCustomerRequest(
+ listOf(
+ CashAppPayPaymentAction.OneTimeAction(
+ amount = 100,
+ currency = CashAppPayCurrency.USD,
+ scopeId = TEST_SCOPE_ID,
+ )
+ ),
+ TEST_RETURN_URL
+ )
+ }
+
+ @Test
+ 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()
+ )
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.onSubmit()
+
+ verify(cashAppPay).createCustomerRequest(
+ listOf(
+ CashAppPayPaymentAction.OneTimeAction(
+ amount = 100,
+ currency = CashAppPayCurrency.USD,
+ scopeId = TEST_SCOPE_ID,
+ )
+ ),
+ TEST_RETURN_URL
+ )
+ }
+
+ @Test
+ 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()
+ )
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ delegate.updateInputData { isStorePaymentSelected = true }
+
+ delegate.onSubmit()
+
+ verify(cashAppPay).createCustomerRequest(
+ listOf(
+ CashAppPayPaymentAction.OnFileAction(scopeId = TEST_SCOPE_ID),
+ ),
+ TEST_RETURN_URL
+ )
+ }
+
+ @Test
+ 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()
+ )
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ verify(cashAppPay).createCustomerRequest(
+ listOf(
+ CashAppPayPaymentAction.OnFileAction(scopeId = TEST_SCOPE_ID),
+ ),
+ TEST_RETURN_URL
+ )
+ }
+
+ @Test
+ 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()
+ )
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.onSubmit()
+
+ // Called once on initialization, but shouldn't be called by onSubmit
+ verify(cashAppPay, times(1)).createCustomerRequest(paymentActions = any(), redirectUri = anyOrNull())
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("amountSource")
+ fun `when updating component state, then amount is propagated in component state if set`(
+ configurationValue: Amount?,
+ expectedComponentStateValue: Amount?,
+ ) = runTest {
+ if (configurationValue != null) {
+ val configuration = getConfigurationBuilder()
+ .setAmount(configurationValue)
+ .build()
+ delegate = createDefaultCashAppPayDelegate(configuration = configuration)
+ }
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.updateComponentState(CashAppPayOutputData(false, null))
+
+ assertEquals(expectedComponentStateValue, testFlow.latestValue.data.amount)
+ }
+
+ @Nested
+ @DisplayName("when cash app pay state changes and state is")
+ inner class CashAppPayStateChangeTest {
+
+ @Test
+ fun `ready to authorize, then the cash app SDK should be used to authorize`() {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.cashAppPayStateDidChange(CashAppPayState.ReadyToAuthorize(mock()))
+
+ verify(cashAppPay).authorizeCustomerRequest()
+ }
+
+ @Test
+ fun `approved, then component state is updated`() = runTest {
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ assertFalse(testFlow.latestValue.isValid)
+
+ // We have to mock this class, because we get an error when using it normally
+ val mockResponse = mock()
+ whenever(mockResponse.grants) doReturn listOf(
+ createGrant(GrantType.ONE_TIME),
+ createGrant(GrantType.EXTENDED)
+ )
+ whenever(mockResponse.customerProfile) doReturn CustomerProfile("customerId", "cashTag")
+ delegate.cashAppPayStateDidChange(CashAppPayState.Approved(mockResponse))
+
+ val actual = testFlow.latestValue
+ assertTrue(actual.isValid)
+ assertEquals("id", actual.data.paymentMethod?.grantId)
+ assertEquals("customerId", actual.data.paymentMethod?.customerId)
+ assertEquals("id", actual.data.paymentMethod?.onFileGrantId)
+ assertEquals("cashTag", actual.data.paymentMethod?.cashtag)
+ }
+
+ @Test
+ fun `approved, then submit handler is called`() = runTest {
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ assertFalse(testFlow.latestValue.isValid)
+
+ // We have to mock this class, because we get an error when using it normally
+ val mockResponse = mock()
+ whenever(mockResponse.grants) doReturn listOf(
+ createGrant(GrantType.ONE_TIME),
+ createGrant(GrantType.EXTENDED)
+ )
+ whenever(mockResponse.customerProfile) doReturn CustomerProfile("customerId", "cashTag")
+ delegate.cashAppPayStateDidChange(CashAppPayState.Approved(mockResponse))
+
+ verify(submitHandler).onSubmit(testFlow.latestValue)
+ }
+
+ @Test
+ fun `declined, then an error is propagated`() = runTest {
+ val testFlow = delegate.exceptionFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ delegate.cashAppPayStateDidChange(CashAppPayState.Declined)
+
+ assertTrue(testFlow.latestValue is ComponentException)
+ }
+
+ @Test
+ fun `exception, then an error is propagated`() = runTest {
+ val testFlow = delegate.exceptionFlow.test(testScheduler)
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ val exception = RuntimeException("Stub!")
+
+ delegate.cashAppPayStateDidChange(CashAppPayState.CashAppPayExceptionState(exception))
+
+ assertEquals(exception, (testFlow.latestValue as ComponentException).cause)
+ }
+
+ private fun createGrant(type: GrantType) = Grant(
+ id = "id",
+ status = "",
+ type = type,
+ action = Action(null, null, "", ""),
+ channel = "",
+ customerId = "",
+ updatedAt = "",
+ createdAt = "",
+ expiresAt = "",
+ )
+ }
+
+ private fun createDefaultCashAppPayDelegate(
+ configuration: CashAppPayConfiguration = getConfigurationBuilder().build()
+ ) = DefaultCashAppPayDelegate(
+ submitHandler = submitHandler,
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = getPaymentMethod(),
+ order = TEST_ORDER,
+ componentParams = CashAppPayComponentParamsMapper(null, null).mapToParams(
+ configuration = configuration,
+ sessionParams = null,
+ paymentMethod = getPaymentMethod(),
+ ),
+ cashAppPayFactory = cashAppPayFactory,
+ ioDispatcher = UnconfinedTestDispatcher(),
+ )
+
+ private fun getConfigurationBuilder() = CashAppPayConfiguration.Builder(
+ shopperLocale = Locale.US,
+ environment = Environment.TEST,
+ clientKey = "test_qwertyuiopasdfghjklzxcvbnmqwerty",
+ )
+ .setReturnUrl(TEST_RETURN_URL)
+
+ private fun getPaymentMethod() = PaymentMethod(
+ configuration = Configuration(
+ clientId = "clientId",
+ scopeId = TEST_SCOPE_ID,
+ ),
+ )
+
+ companion object {
+ private val TEST_ORDER = OrderRequest("PSP", "ORDER_DATA")
+ private const val TEST_RETURN_URL = "testReturnUrl"
+ private const val TEST_SCOPE_ID = "testScopeId"
+
+ @JvmStatic
+ fun amountSource() = listOf(
+ // configurationValue, expectedComponentStateValue
+ arguments(Amount("EUR", 100), Amount("EUR", 100)),
+ arguments(Amount("USD", 0), Amount("USD", 0)),
+ arguments(Amount.EMPTY, null),
+ arguments(null, null),
+ )
+ }
+}
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
new file mode 100644
index 0000000000..5593e0f348
--- /dev/null
+++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/StoredCashAppPayDelegateTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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 4/7/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui
+
+import com.adyen.checkout.cashapppay.CashAppPayComponentState
+import com.adyen.checkout.cashapppay.CashAppPayConfiguration
+import com.adyen.checkout.cashapppay.internal.ui.model.CashAppPayComponentParamsMapper
+import com.adyen.checkout.components.core.Amount
+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.paymentmethod.CashAppPayPaymentMethod
+import com.adyen.checkout.core.Environment
+import com.adyen.checkout.test.extensions.test
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions.assertEquals
+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 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.verify
+import java.util.Locale
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(MockitoExtension::class)
+internal class StoredCashAppPayDelegateTest(
+ @Mock private val analyticsRepository: AnalyticsRepository,
+) {
+
+ private lateinit var delegate: StoredCashAppPayDelegate
+
+ @BeforeEach
+ fun before() {
+ delegate = createStoredCashAppPayDelegate()
+ }
+
+ @Test
+ fun `when delegate is initialized, then state is valid`() = runTest {
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+ with(testFlow.latestValue) {
+ assertTrue(isInputValid)
+ assertTrue(isReady)
+ assertTrue(isValid)
+ }
+ }
+
+ @Test
+ fun `when delegate is initialized, then analytics event is sent`() = runTest {
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+ verify(analyticsRepository).sendAnalyticsEvent()
+ }
+
+ @ParameterizedTest
+ @MethodSource("amountSource")
+ fun `when input data is valid, then amount is propagated in component state if set`(
+ configurationValue: Amount?,
+ expectedComponentStateValue: Amount?,
+ ) = runTest {
+ if (configurationValue != null) {
+ val configuration = getConfigurationBuilder()
+ .setAmount(configurationValue)
+ .build()
+ delegate = createStoredCashAppPayDelegate(configuration = configuration)
+ }
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ assertEquals(expectedComponentStateValue, testFlow.latestValue.data.amount)
+ }
+
+ @Test
+ fun `when delegate is initialized, then submit handler onSubmit is called`() = runTest {
+ val testFlow = delegate.submitFlow.test(testScheduler)
+
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ assertEquals(delegate.componentStateFlow.first(), testFlow.latestValue)
+ }
+
+ @Test
+ fun `when delegate is initialized, then component state is created correctly`() = runTest {
+ val testFlow = delegate.componentStateFlow.test(testScheduler)
+
+ delegate.initialize(CoroutineScope(UnconfinedTestDispatcher()))
+
+ val expected = CashAppPayComponentState(
+ data = PaymentComponentData(
+ paymentMethod = CashAppPayPaymentMethod(
+ type = TEST_PAYMENT_METHOD_TYPE,
+ storedPaymentMethodId = TEST_PAYMENT_METHOD_ID,
+ ),
+ order = TEST_ORDER,
+ amount = null,
+ ),
+ isInputValid = true,
+ isReady = true
+ )
+ assertEquals(expected, testFlow.latestValue)
+ }
+
+ private fun createStoredCashAppPayDelegate(
+ configuration: CashAppPayConfiguration = getConfigurationBuilder().build()
+ ) = StoredCashAppPayDelegate(
+ analyticsRepository = analyticsRepository,
+ observerRepository = PaymentObserverRepository(),
+ paymentMethod = StoredPaymentMethod(
+ id = TEST_PAYMENT_METHOD_ID,
+ type = TEST_PAYMENT_METHOD_TYPE,
+ ),
+ order = TEST_ORDER,
+ componentParams = CashAppPayComponentParamsMapper(null, null).mapToParams(
+ configuration = configuration,
+ sessionParams = null,
+ paymentMethod = StoredPaymentMethod(),
+ ),
+ )
+
+ private fun getConfigurationBuilder() = CashAppPayConfiguration.Builder(
+ shopperLocale = Locale.US,
+ environment = Environment.TEST,
+ clientKey = "test_qwertyuiopasdfghjklzxcvbnmqwerty",
+ )
+ .setReturnUrl("test")
+
+ companion object {
+ 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"
+
+ @JvmStatic
+ fun amountSource() = listOf(
+ // configurationValue, expectedComponentStateValue
+ arguments(Amount("EUR", 100), Amount("EUR", 100)),
+ arguments(Amount("USD", 0), Amount("USD", 0)),
+ arguments(Amount.EMPTY, null),
+ arguments(null, null),
+ )
+ }
+}
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
new file mode 100644
index 0000000000..76ff50ab70
--- /dev/null
+++ b/cashapppay/src/test/java/com/adyen/checkout/cashapppay/internal/ui/model/CashAppPayComponentParamsMapperTest.kt
@@ -0,0 +1,337 @@
+/*
+ * 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 4/7/2023.
+ */
+
+package com.adyen.checkout.cashapppay.internal.ui.model
+
+import com.adyen.checkout.cashapppay.CashAppPayConfiguration
+import com.adyen.checkout.cashapppay.CashAppPayEnvironment
+import com.adyen.checkout.components.core.Amount
+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.GenericComponentParams
+import com.adyen.checkout.components.core.internal.ui.model.SessionParams
+import com.adyen.checkout.core.Environment
+import com.adyen.checkout.core.exception.ComponentException
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+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 java.util.Locale
+
+internal class CashAppPayComponentParamsMapperTest {
+
+ @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,
+ paymentMethod = getDefaultPaymentMethod(),
+ )
+
+ val expected = getComponentParams()
+
+ assertEquals(expected, params)
+ }
+
+ @Test
+ fun `when parent configuration is null and custom configuration fields are set then all fields should match`() {
+ val configuration = CashAppPayConfiguration.Builder(
+ 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,
+ paymentMethod = getDefaultPaymentMethod(),
+ )
+
+ val expected = getComponentParams(
+ isSubmitButtonVisible = false,
+ shopperLocale = Locale.FRANCE,
+ environment = Environment.APSE,
+ clientKey = TEST_CLIENT_KEY_2,
+ cashAppPayEnvironment = CashAppPayEnvironment.PRODUCTION,
+ returnUrl = "https://google.com",
+ showStorePaymentField = false,
+ storePaymentMethod = true,
+ )
+
+ assertEquals(expected, params)
+ }
+
+ @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(
+ shopperLocale = Locale.GERMAN,
+ environment = Environment.EUROPE,
+ clientKey = TEST_CLIENT_KEY_2,
+ isAnalyticsEnabled = false,
+ isCreatedByDropIn = true,
+ amount = Amount(
+ currency = "CAD",
+ value = 1235_00L
+ )
+ )
+
+ val params = CashAppPayComponentParamsMapper(overrideParams, null).mapToParams(
+ configuration = configuration,
+ sessionParams = null,
+ paymentMethod = getDefaultPaymentMethod(),
+ )
+
+ val expected = getComponentParams(
+ shopperLocale = Locale.GERMAN,
+ environment = Environment.EUROPE,
+ clientKey = TEST_CLIENT_KEY_2,
+ isAnalyticsEnabled = false,
+ isCreatedByDropIn = true,
+ amount = Amount(
+ currency = "CAD",
+ value = 1235_00L
+ )
+ )
+
+ assertEquals(expected, params)
+ }
+
+ @ParameterizedTest
+ @MethodSource("enableStoreDetailsSource")
+ fun `showStorePaymentField should match value set in sessions if it exists, otherwise should match configuration`(
+ configurationValue: Boolean,
+ 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,
+ installmentOptions = null,
+ amount = null,
+ returnUrl = TEST_RETURN_URL,
+ ),
+ paymentMethod = getDefaultPaymentMethod(),
+ )
+
+ val expected = getComponentParams(
+ showStorePaymentField = expectedValue
+ )
+
+ 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`(
+ 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,
+ installmentOptions = null,
+ amount = sessionsValue,
+ returnUrl = TEST_RETURN_URL,
+ ),
+ getDefaultPaymentMethod(),
+ )
+
+ val expected = getComponentParams(
+ amount = expectedValue
+ )
+
+ assertEquals(expected, params)
+ }
+
+ @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,
+ paymentMethod = getDefaultPaymentMethod(),
+ )
+ }
+ }
+
+ @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"),
+ paymentMethod = getDefaultPaymentMethod(),
+ )
+
+ assertEquals("sessionReturnUrl", params.returnUrl)
+ }
+
+ @Test
+ fun `when clientId is not available, then an exception is thrown`() {
+ assertThrows {
+ val configuration = getConfigurationBuilder()
+ .setReturnUrl(TEST_RETURN_URL)
+ .build()
+
+ CashAppPayComponentParamsMapper(null, null).mapToParams(
+ configuration = configuration,
+ sessionParams = null,
+ paymentMethod = PaymentMethod(
+ configuration = Configuration(clientId = null, scopeId = TEST_SCOPE_ID)
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `when scopeId is not available, then an exception is thrown`() {
+ assertThrows {
+ val configuration = getConfigurationBuilder()
+ .setReturnUrl(TEST_RETURN_URL)
+ .build()
+
+ CashAppPayComponentParamsMapper(null, null).mapToParams(
+ configuration = configuration,
+ sessionParams = null,
+ paymentMethod = PaymentMethod(
+ configuration = Configuration(clientId = TEST_CLIENT_ID, scopeId = null)
+ ),
+ )
+ }
+ }
+
+ @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 expected = getComponentParams(
+ clientId = null,
+ scopeId = null,
+ )
+
+ assertEquals(expected, params)
+ }
+
+ @Suppress("LongParameterList")
+ private fun getComponentParams(
+ shopperLocale: Locale = Locale.US,
+ environment: Environment = Environment.TEST,
+ clientKey: String = TEST_CLIENT_KEY_1,
+ isAnalyticsEnabled: Boolean = true,
+ isCreatedByDropIn: Boolean = false,
+ amount: Amount = Amount.EMPTY,
+ isSubmitButtonVisible: Boolean = true,
+ cashAppPayEnvironment: CashAppPayEnvironment = CashAppPayEnvironment.SANDBOX,
+ returnUrl: String = TEST_RETURN_URL,
+ showStorePaymentField: Boolean = true,
+ storePaymentMethod: Boolean = false,
+ clientId: String? = TEST_CLIENT_ID,
+ scopeId: String? = TEST_SCOPE_ID,
+ ) = CashAppPayComponentParams(
+ isSubmitButtonVisible = isSubmitButtonVisible,
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey,
+ isAnalyticsEnabled = isAnalyticsEnabled,
+ isCreatedByDropIn = isCreatedByDropIn,
+ amount = amount,
+ cashAppPayEnvironment = cashAppPayEnvironment,
+ returnUrl = returnUrl,
+ showStorePaymentField = showStorePaymentField,
+ storePaymentMethod = storePaymentMethod,
+ clientId = clientId,
+ scopeId = scopeId,
+ )
+
+ private fun getConfigurationBuilder() = CashAppPayConfiguration.Builder(
+ shopperLocale = Locale.US,
+ environment = Environment.TEST,
+ clientKey = TEST_CLIENT_KEY_1
+ )
+
+ private fun getDefaultPaymentMethod() = PaymentMethod(
+ configuration = Configuration(clientId = TEST_CLIENT_ID, scopeId = TEST_SCOPE_ID)
+ )
+
+ companion object {
+ private const val TEST_CLIENT_KEY_1 = "test_qwertyuiopasdfghjklzxcvbnmqwerty"
+ private const val TEST_CLIENT_KEY_2 = "live_qwertyui34566776787zxcvbnmqwerty"
+ 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"
+
+ @JvmStatic
+ fun enableStoreDetailsSource() = listOf(
+ // configurationValue, sessionsValue, expectedValue
+ arguments(false, false, false),
+ arguments(false, true, true),
+ arguments(true, false, false),
+ arguments(true, true, true),
+ arguments(false, null, false),
+ arguments(true, null, true),
+ )
+
+ @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)),
+ )
+ }
+}
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 180fabf4c9..65ba945505 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
@@ -8,14 +8,10 @@
package com.adyen.checkout.core.exception
-import androidx.annotation.RestrictTo
-
/**
*
* This exception indicates that the required runtime permission is not granted.
*/
-
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class PermissionException(
errorMessage: String,
val requiredPermission: String
diff --git a/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/model/ModelObject.kt b/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/model/ModelObject.kt
index bc56dab1ce..5ac288cbfb 100644
--- a/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/model/ModelObject.kt
+++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/data/model/ModelObject.kt
@@ -20,8 +20,9 @@ import org.json.JSONObject
* The classes extending [ModelObject] are data classes designed to work standalone or in association with JSON
* libraries like GSON and Moshi.
*/
+abstract class ModelObject
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class ModelObject : Parcelable {
+constructor() : Parcelable {
override fun describeContents(): Int {
return Parcelable.CONTENTS_FILE_DESCRIPTOR
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
new file mode 100644
index 0000000000..e378f1eb45
--- /dev/null
+++ b/checkout-core/src/main/java/com/adyen/checkout/core/internal/util/RunCompileOnly.kt
@@ -0,0 +1,24 @@
+/*
+ * 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/5/2023.
+ */
+
+package com.adyen.checkout.core.internal.util
+
+import androidx.annotation.RestrictTo
+
+@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)
+ } catch (e: NoClassDefFoundError) {
+ Logger.w(LogUtil.getTag(), "Class not found. Are you missing a dependency?", e)
+ }
+
+ return null
+}
diff --git a/components-compose/build.gradle b/components-compose/build.gradle
new file mode 100644
index 0000000000..fe541590e6
--- /dev/null
+++ b/components-compose/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * 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 1/6/2023.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+// Maven artifact
+ext.mavenArtifactId = "components-compose"
+ext.mavenArtifactName = "Adyen checkout components compose"
+ext.mavenArtifactDescription = "Compose compat Adyen checkout components."
+
+apply from: "${rootDir}/config/gradle/sharedTasks.gradle"
+
+android {
+ namespace 'com.adyen.checkout.components.compose'
+ compileSdkVersion compile_sdk_version
+
+ defaultConfig {
+ minSdkVersion min_sdk_version
+ targetSdkVersion target_sdk_version
+ versionCode version_code
+ versionName version_name
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = compose_compiler_version
+ }
+}
+
+dependencies {
+ // Checkout
+ api project(':components-core')
+ api project(':sessions-core')
+ api project(':ui-core')
+
+ implementation platform(libraries.compose.bom)
+ implementation libraries.compose.viewmodel
+}
diff --git a/components-compose/consumer-rules.pro b/components-compose/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/components-compose/src/main/AndroidManifest.xml b/components-compose/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..c62a023f8b
--- /dev/null
+++ b/components-compose/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
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
new file mode 100644
index 0000000000..ce95a86bf8
--- /dev/null
+++ b/components-compose/src/main/java/com/adyen/checkout/components/compose/ComposeExtensions.kt
@@ -0,0 +1,222 @@
+/*
+ * 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 josephj on 17/5/2023.
+ */
+
+package com.adyen.checkout.components.compose
+
+import android.app.Application
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+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.ComponentCallback
+import com.adyen.checkout.components.core.Order
+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.components.core.internal.Component
+import com.adyen.checkout.components.core.internal.Configuration
+import com.adyen.checkout.components.core.internal.PaymentComponent
+import com.adyen.checkout.components.core.internal.provider.PaymentComponentProvider
+import com.adyen.checkout.components.core.internal.provider.StoredPaymentComponentProvider
+import com.adyen.checkout.core.exception.ComponentException
+import com.adyen.checkout.sessions.core.CheckoutSession
+import com.adyen.checkout.sessions.core.SessionComponentCallback
+import com.adyen.checkout.sessions.core.internal.provider.SessionPaymentComponentProvider
+import com.adyen.checkout.sessions.core.internal.provider.SessionStoredPaymentComponentProvider
+import com.adyen.checkout.ui.core.AdyenComponentView
+import com.adyen.checkout.ui.core.internal.ui.ViewableComponent
+
+/**
+ * Get a [PaymentComponent] from a [Composable].
+ *
+ * @param paymentMethod The corresponding [PaymentMethod] object.
+ * @param configuration The Configuration of the component.
+ * @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,
+ configuration: ConfigurationT,
+ 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,
+ configuration = configuration,
+ 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 configuration The Configuration of the component.
+ * @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,
+ configuration: ConfigurationT,
+ 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,
+ configuration = configuration,
+ 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 configuration The Configuration of the component.
+ * @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,
+ configuration: ConfigurationT,
+ 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,
+ configuration = configuration,
+ 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 configuration The Configuration of the component.
+ * @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,
+ configuration: ConfigurationT,
+ 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,
+ configuration = configuration,
+ application = LocalContext.current.applicationContext as Application,
+ componentCallback = componentCallback,
+ key = key,
+ )
+}
+
+/**
+ * A [Composable] that can display input and fill in details for a [Component].
+ */
+@Suppress("unused")
+@Composable
+fun AdyenComponent(
+ component: T,
+ modifier: Modifier = Modifier,
+) where T : ViewableComponent, T : Component {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ AndroidView(
+ factory = {
+ AdyenComponentView(it).apply {
+ attach(component, lifecycleOwner)
+ }
+ },
+ modifier = modifier,
+ )
+}
diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/Configuration.kt b/components-core/src/main/java/com/adyen/checkout/components/core/Configuration.kt
index 9d75de0d05..5eced22bfc 100644
--- a/components-core/src/main/java/com/adyen/checkout/components/core/Configuration.kt
+++ b/components-core/src/main/java/com/adyen/checkout/components/core/Configuration.kt
@@ -20,6 +20,8 @@ data class Configuration(
var gatewayMerchantId: String? = null,
var intent: String? = null,
var koreanAuthenticationRequired: String? = null,
+ var clientId: String? = null,
+ var scopeId: String? = null,
) : ModelObject() {
companion object {
@@ -34,6 +36,10 @@ data class Configuration(
// Card
private const val KOREAN_AUTHENTICATION_REQUIRED = "koreanAuthenticationRequired"
+ // Cash App Pay
+ private const val CLIENT_ID = "clientId"
+ private const val SCOPE_ID = "scopeId"
+
@JvmField
val SERIALIZER: Serializer = object : Serializer {
override fun serialize(modelObject: Configuration): JSONObject {
@@ -43,6 +49,8 @@ data class Configuration(
putOpt(GATEWAY_MERCHANT_ID, modelObject.gatewayMerchantId)
putOpt(INTENT, modelObject.intent)
putOpt(KOREAN_AUTHENTICATION_REQUIRED, modelObject.koreanAuthenticationRequired)
+ putOpt(CLIENT_ID, modelObject.clientId)
+ putOpt(SCOPE_ID, modelObject.scopeId)
}
} catch (e: JSONException) {
throw ModelSerializationException(PaymentMethod::class.java, e)
@@ -54,7 +62,9 @@ data class Configuration(
merchantId = jsonObject.getStringOrNull(MERCHANT_ID),
gatewayMerchantId = jsonObject.getStringOrNull(GATEWAY_MERCHANT_ID),
intent = jsonObject.getStringOrNull(INTENT),
- koreanAuthenticationRequired = jsonObject.getStringOrNull(KOREAN_AUTHENTICATION_REQUIRED)
+ koreanAuthenticationRequired = jsonObject.getStringOrNull(KOREAN_AUTHENTICATION_REQUIRED),
+ clientId = jsonObject.getStringOrNull(CLIENT_ID),
+ scopeId = jsonObject.getStringOrNull(SCOPE_ID),
)
}
}
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 c5779105fd..7d59b779e5 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
@@ -7,8 +7,6 @@
*/
package com.adyen.checkout.components.core
-import java.util.Collections
-
/**
* Helper class with a list of all the currently supported Payment Methods on Components and Drop-In.
*/
@@ -19,155 +17,155 @@ object PaymentMethodTypes {
// Type of the payment method as received by the paymentMethods/ API
const val ACH = "ach"
- const val IDEAL = "ideal"
- const val MOLPAY_MALAYSIA = "molpay_ebanking_fpx_MY"
- const val MOLPAY_THAILAND = "molpay_ebanking_TH"
- const val MOLPAY_VIETNAM = "molpay_ebanking_VN"
+ const val BACS = "directdebit_GB"
+ const val BCMC = "bcmc"
+ const val BLIK = "blik"
+ const val BOLETOBANCARIO = "boletobancario"
+ const val BOLETOBANCARIO_BANCODOBRASIL = "boletobancario_bancodobrasil"
+ const val BOLETOBANCARIO_BRADESCO = "boletobancario_bradesco"
+ const val BOLETOBANCARIO_HSBC = "boletobancario_hsbc"
+ const val BOLETOBANCARIO_ITAU = "boletobancario_itau"
+ const val BOLETOBANCARIO_SANTANDER = "boletobancario_santander"
+ const val BOLETO_PRIMEIRO_PAY = "primeiropay_boleto"
+ const val CASH_APP_PAY = "cashapp"
const val DOTPAY = "dotpay"
- const val EPS = "eps"
const val ENTERCASH = "entercash"
- const val OPEN_BANKING = "openbanking_UK"
- const val SCHEME = "scheme"
+ const val EPS = "eps"
+ const val GIFTCARD = "giftcard"
const val GOOGLE_PAY = "googlepay"
const val GOOGLE_PAY_LEGACY = "paywithgoogle"
- const val SEPA = "sepadirectdebit"
- const val BACS = "directdebit_GB"
- const val BCMC = "bcmc"
+ const val IDEAL = "ideal"
const val MB_WAY = "mbway"
- const val BLIK = "blik"
- const val GIFTCARD = "giftcard"
+ const val MOLPAY_MALAYSIA = "molpay_ebanking_fpx_MY"
+ const val MOLPAY_THAILAND = "molpay_ebanking_TH"
+ const val MOLPAY_VIETNAM = "molpay_ebanking_VN"
const val ONLINE_BANKING_CZ = "onlineBanking_CZ"
const val ONLINE_BANKING_PL = "onlineBanking_PL"
const val ONLINE_BANKING_SK = "onlineBanking_SK"
+ const val OPEN_BANKING = "openbanking_UK"
const val PAY_BY_BANK = "paybybank"
+ const val SCHEME = "scheme"
+ const val SEPA = "sepadirectdebit"
const val UPI = "upi"
const val UPI_COLLECT = "upi_collect"
const val UPI_QR = "upi_qr"
// Payment methods that do not need a payment component, but only an action component
const val DUIT_NOW = "duitnow"
- const val WECHAT_PAY_SDK = "wechatpaySDK"
const val PAY_NOW = "paynow"
const val PIX = "pix"
const val PROMPT_PAY = "promptpay"
+ const val WECHAT_PAY_SDK = "wechatpaySDK"
// Voucher payment methods that are not yet supported
- const val MULTIBANCO = "multibanco"
- const val OXXO = "oxxo"
const val DOKU = "doku"
const val DOKU_ALFMART = "doku_alfamart"
- const val DOKU_PERMATA_LITE_ATM = "doku_permata_lite_atm"
- const val DOKU_INDOMARET = "doku_indomaret"
const val DOKU_ATM_MANDIRI_VA = "doku_atm_mandiri_va"
- const val DOKU_SINARMAS_VA = "doku_sinarmas_va"
- const val DOKU_MANDIRI_VA = "doku_mandiri_va"
+ const val DOKU_BCA_VA = "doku_bca_va"
+ const val DOKU_BNI_VA = "doku_bni_va"
+ const val DOKU_BRI_VA = "doku_bri_va"
const val DOKU_CIMB_VA = "doku_cimb_va"
const val DOKU_DANAMON_VA = "doku_danamon_va"
- const val DOKU_BRI_VA = "doku_bri_va"
- const val DOKU_BNI_VA = "doku_bni_va"
- const val DOKU_BCA_VA = "doku_bca_va"
+ const val DOKU_INDOMARET = "doku_indomaret"
+ const val DOKU_MANDIRI_VA = "doku_mandiri_va"
+ const val DOKU_PERMATA_LITE_ATM = "doku_permata_lite_atm"
+ const val DOKU_SINARMAS_VA = "doku_sinarmas_va"
const val DOKU_WALLET = "doku_wallet"
- const val BOLETOBANCARIO = "boletobancario"
- const val BOLETOBANCARIO_BANCODOBRASIL = "boletobancario_bancodobrasil"
- const val BOLETOBANCARIO_BRADESCO = "boletobancario_bradesco"
- const val BOLETOBANCARIO_HSBC = "boletobancario_hsbc"
- const val BOLETOBANCARIO_ITAU = "boletobancario_itau"
- const val BOLETOBANCARIO_SANTANDER = "boletobancario_santander"
const val DRAGONPAY_EBANKING = "dragonpay_ebanking"
const val DRAGONPAY_OTC_BANKING = "dragonpay_otc_banking"
const val DRAGONPAY_OTC_NON_BANKING = "dragonpay_otc_non_banking"
const val DRAGONPAY_OTC_PHILIPPINES = "dragonpay_otc_philippines"
- const val ECONTEXT_SEVEN_ELEVEN = "econtext_seven_eleven"
const val ECONTEXT_ATM = "econtext_atm"
- const val ECONTEXT_STORES = "econtext_stores"
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
- const val BCMC_QR = "bcmc_mobile_QR"
const val AFTER_PAY = "afterpay_default"
+ const val BCMC_QR = "bcmc_mobile_QR"
const val WECHAT_PAY_MINI_PROGRAM = "wechatpayMiniProgram"
const val WECHAT_PAY_QR = "wechatpayQR"
const val WECHAT_PAY_WEB = "wechatpayWeb"
// List of all payment method types.
- val SUPPORTED_PAYMENT_METHODS: List = Collections.unmodifiableList(
- listOf(
- ACH,
- BCMC,
- DUIT_NOW,
- DOTPAY,
- ENTERCASH,
- EPS,
- GIFTCARD,
- GOOGLE_PAY,
- GOOGLE_PAY_LEGACY,
- IDEAL,
- MB_WAY,
- MOLPAY_MALAYSIA,
- MOLPAY_THAILAND,
- MOLPAY_VIETNAM,
- OPEN_BANKING,
- PAY_BY_BANK,
- SEPA,
- BACS,
- SCHEME,
- BLIK,
- WECHAT_PAY_SDK,
- PAY_NOW,
- ONLINE_BANKING_CZ,
- ONLINE_BANKING_PL,
- PIX,
- PROMPT_PAY,
- UPI,
- UPI_COLLECT,
- UPI_QR,
- )
+ val SUPPORTED_PAYMENT_METHODS: List = listOf(
+ ACH,
+ BACS,
+ BCMC,
+ BLIK,
+ BOLETOBANCARIO,
+ BOLETOBANCARIO_BANCODOBRASIL,
+ BOLETOBANCARIO_BRADESCO,
+ BOLETOBANCARIO_HSBC,
+ BOLETOBANCARIO_ITAU,
+ BOLETOBANCARIO_SANTANDER,
+ BOLETO_PRIMEIRO_PAY,
+ CASH_APP_PAY,
+ DOTPAY,
+ DUIT_NOW,
+ ENTERCASH,
+ EPS,
+ GIFTCARD,
+ GOOGLE_PAY,
+ GOOGLE_PAY_LEGACY,
+ IDEAL,
+ MB_WAY,
+ MOLPAY_MALAYSIA,
+ MOLPAY_THAILAND,
+ MOLPAY_VIETNAM,
+ ONLINE_BANKING_CZ,
+ ONLINE_BANKING_PL,
+ OPEN_BANKING,
+ PAY_BY_BANK,
+ PAY_NOW,
+ PIX,
+ PROMPT_PAY,
+ SCHEME,
+ SEPA,
+ UPI,
+ UPI_COLLECT,
+ UPI_QR,
+ WECHAT_PAY_SDK,
)
- val SUPPORTED_ACTION_ONLY_PAYMENT_METHODS: List = Collections.unmodifiableList(
- listOf(
- DUIT_NOW,
- WECHAT_PAY_SDK,
- PAY_NOW,
- PIX,
- PROMPT_PAY
- )
+
+ val SUPPORTED_ACTION_ONLY_PAYMENT_METHODS: List = listOf(
+ DUIT_NOW,
+ PAY_NOW,
+ PIX,
+ PROMPT_PAY,
+ WECHAT_PAY_SDK,
)
- val UNSUPPORTED_PAYMENT_METHODS: List = Collections.unmodifiableList(
- listOf(
- BCMC_QR,
- AFTER_PAY,
- WECHAT_PAY_MINI_PROGRAM,
- WECHAT_PAY_QR,
- WECHAT_PAY_WEB,
- MULTIBANCO,
- OXXO,
- DOKU,
- DOKU_ALFMART,
- DOKU_PERMATA_LITE_ATM,
- DOKU_INDOMARET,
- DOKU_ATM_MANDIRI_VA,
- DOKU_SINARMAS_VA,
- DOKU_MANDIRI_VA,
- DOKU_CIMB_VA,
- DOKU_DANAMON_VA,
- DOKU_BRI_VA,
- DOKU_BNI_VA,
- DOKU_BCA_VA,
- DOKU_WALLET,
- ECONTEXT_ATM,
- ECONTEXT_ONLINE,
- ECONTEXT_SEVEN_ELEVEN,
- ECONTEXT_STORES,
- BOLETOBANCARIO,
- BOLETOBANCARIO_BANCODOBRASIL,
- BOLETOBANCARIO_BRADESCO,
- BOLETOBANCARIO_HSBC,
- BOLETOBANCARIO_ITAU,
- BOLETOBANCARIO_SANTANDER,
- DRAGONPAY_EBANKING,
- DRAGONPAY_OTC_BANKING,
- DRAGONPAY_OTC_NON_BANKING,
- DRAGONPAY_OTC_PHILIPPINES,
- )
+
+ val UNSUPPORTED_PAYMENT_METHODS: List = listOf(
+ AFTER_PAY,
+ BCMC_QR,
+ DOKU,
+ DOKU_ALFMART,
+ DOKU_ATM_MANDIRI_VA,
+ DOKU_BCA_VA,
+ DOKU_BNI_VA,
+ DOKU_BRI_VA,
+ DOKU_CIMB_VA,
+ DOKU_DANAMON_VA,
+ DOKU_INDOMARET,
+ DOKU_MANDIRI_VA,
+ DOKU_PERMATA_LITE_ATM,
+ DOKU_SINARMAS_VA,
+ DOKU_WALLET,
+ DRAGONPAY_EBANKING,
+ 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,
+ WECHAT_PAY_WEB,
)
}
diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/StoredPaymentMethod.kt b/components-core/src/main/java/com/adyen/checkout/components/core/StoredPaymentMethod.kt
index 6ed4befdca..59d76c43cf 100644
--- a/components-core/src/main/java/com/adyen/checkout/components/core/StoredPaymentMethod.kt
+++ b/components-core/src/main/java/com/adyen/checkout/components/core/StoredPaymentMethod.kt
@@ -28,7 +28,8 @@ data class StoredPaymentMethod(
var lastFour: String? = null,
var shopperEmail: String? = null,
var supportedShopperInteractions: List? = null,
- var bankAccountNumber: String? = null
+ var bankAccountNumber: String? = null,
+ var cashtag: String? = null,
) : ModelObject() {
val isEcommerce: Boolean
@@ -47,6 +48,7 @@ data class StoredPaymentMethod(
private const val SUPPORTED_SHOPPER_INTERACTIONS = "supportedShopperInteractions"
private const val ECOMMERCE = "Ecommerce"
private const val BANK_ACCOUNT_NUMBER = "bankAccountNumber"
+ private const val CASH_TAG = "cashtag"
@JvmField
val SERIALIZER: Serializer = object : Serializer {
@@ -64,6 +66,7 @@ data class StoredPaymentMethod(
putOpt(SHOPPER_EMAIL, modelObject.shopperEmail)
putOpt(SUPPORTED_SHOPPER_INTERACTIONS, JSONArray(modelObject.supportedShopperInteractions))
putOpt(BANK_ACCOUNT_NUMBER, modelObject.bankAccountNumber)
+ putOpt(CASH_TAG, modelObject.cashtag)
}
} catch (e: JSONException) {
throw ModelSerializationException(StoredPaymentMethod::class.java, e)
@@ -85,6 +88,7 @@ data class StoredPaymentMethod(
jsonObject.optJSONArray(SUPPORTED_SHOPPER_INTERACTIONS)
),
bankAccountNumber = jsonObject.getStringOrNull(BANK_ACCOUNT_NUMBER),
+ cashtag = jsonObject.getStringOrNull(CASH_TAG),
)
}
}
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 83dc0f414c..ae7dc811d9 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
@@ -30,7 +30,9 @@ data class VoucherAction(
var reference: String? = null,
var alternativeReference: String? = null,
var merchantName: String? = null,
+ // TODO: remove url when it's fixed from backend side
var url: String? = null,
+ var downloadUrl: String? = null
) : Action() {
companion object {
@@ -44,6 +46,7 @@ data class VoucherAction(
private const val ALTERNATIVE_REFERENCE = "alternativeReference"
private const val MERCHANT_NAME = "merchantName"
private const val URL = "url"
+ private const val DOWNLOAD_URL = "downloadUrl"
@JvmField
val SERIALIZER: Serializer = object : Serializer {
@@ -62,6 +65,7 @@ data class VoucherAction(
putOpt(ALTERNATIVE_REFERENCE, modelObject.alternativeReference)
putOpt(MERCHANT_NAME, modelObject.merchantName)
putOpt(URL, modelObject.url)
+ putOpt(DOWNLOAD_URL, modelObject.downloadUrl)
}
} catch (e: JSONException) {
throw ModelSerializationException(VoucherAction::class.java, e)
@@ -82,6 +86,7 @@ data class VoucherAction(
alternativeReference = jsonObject.getStringOrNull(ALTERNATIVE_REFERENCE),
merchantName = jsonObject.getStringOrNull(MERCHANT_NAME),
url = jsonObject.getStringOrNull(URL),
+ downloadUrl = jsonObject.getStringOrNull(DOWNLOAD_URL),
)
}
}
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
new file mode 100644
index 0000000000..e7e249989c
--- /dev/null
+++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/NotAvailablePaymentMethod.kt
@@ -0,0 +1,27 @@
+/*
+ * 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 9/5/2023.
+ */
+
+package com.adyen.checkout.components.core.internal
+
+import android.app.Application
+import androidx.annotation.RestrictTo
+import com.adyen.checkout.components.core.ComponentAvailableCallback
+import com.adyen.checkout.components.core.PaymentMethod
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class NotAvailablePaymentMethod : PaymentMethodAvailabilityCheck {
+
+ override fun isAvailable(
+ applicationContext: Application,
+ paymentMethod: PaymentMethod,
+ configuration: Configuration?,
+ callback: ComponentAvailableCallback
+ ) {
+ callback.onAvailabilityResult(false, paymentMethod)
+ }
+}
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 4b22b23d43..2ac257dc31 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
@@ -16,4 +16,5 @@ data class SessionParams(
val enableStoreDetails: Boolean?,
val installmentOptions: Map?,
val amount: Amount?,
+ val returnUrl: String?,
)
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 b84f859dd1..7ff3224295 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,7 +9,9 @@
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 java.text.DateFormat
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
@@ -17,6 +19,9 @@ import java.util.Locale
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
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 {
@@ -46,8 +51,31 @@ object DateUtils {
dateFormat.parse(date)
true
} catch (e: ParseException) {
- Logger.e("DateUtil", "Provided date $date does not match the given format $format")
+ Logger.e(TAG, "Provided date $date does not match the given format $format")
false
}
}
+
+ /**
+ * Format server date pattern to regular date pattern (30/03/2023).
+ *
+ * @param date date value coming from server
+ * @param shopperLocale
+ * @param inputFormat server date pattern
+ */
+ fun formatStringDate(
+ 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
+ }
+ }
}
diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/ValidationUtils.kt b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/ValidationUtils.kt
index e2587561a3..3aa91160a8 100644
--- a/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/ValidationUtils.kt
+++ b/components-core/src/main/java/com/adyen/checkout/components/core/internal/util/ValidationUtils.kt
@@ -9,9 +9,9 @@ import java.util.regex.Pattern
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
object ValidationUtils {
- @Suppress("ktlint:max-line-length", "MaxLineLength")
+ @Suppress("ktlint:standard:max-line-length", "MaxLineLength")
private const val EMAIL_REGEX =
- "^(([a-zA-Z0-9!#\$%&'\\*\\+\\-\\/=\\?\\^_`\\{\\|\\}~]+(\\.[a-zA-Z0-9!#\$%&'\\*\\+\\-\\/=\\?\\^_`\\{\\|\\}~]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|((([a-zA-Z0-9]+[\\-]*)*[a-zA-Z0-9]+\\.)+[a-zA-Z]{2,}))$"
+ "^(([a-z0-9!#$%&'*+\\-/=?^_`{|}~]+(\\.[a-z0-9!#$%&'*+\\-/=?^_`{|}~]+)*)|(\".+\"))@((\\[((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}])|((?!-)[a-z0-9-]{1,63}(? = object : Serializer {
+ override fun serialize(modelObject: CashAppPayPaymentMethod): JSONObject = JSONObject().apply {
+ try {
+ putOpt(TYPE, modelObject.type)
+ putOpt(GRANT_ID, modelObject.grantId)
+ putOpt(ON_FILE_GRANT_ID, modelObject.onFileGrantId)
+ putOpt(CUSTOMER_ID, modelObject.customerId)
+ putOpt(CASH_TAG, modelObject.cashtag)
+ putOpt(STORED_PAYMENT_METHOD_ID, modelObject.storedPaymentMethodId)
+ } catch (e: JSONException) {
+ throw ModelSerializationException(CashAppPayPaymentMethod::class.java, e)
+ }
+ }
+
+ override fun deserialize(jsonObject: JSONObject): CashAppPayPaymentMethod = CashAppPayPaymentMethod(
+ type = jsonObject.getStringOrNull(TYPE),
+ grantId = jsonObject.getStringOrNull(GRANT_ID),
+ onFileGrantId = jsonObject.getStringOrNull(ON_FILE_GRANT_ID),
+ customerId = jsonObject.getStringOrNull(CUSTOMER_ID),
+ cashtag = jsonObject.getStringOrNull(CASH_TAG),
+ storedPaymentMethodId = jsonObject.getStringOrNull(STORED_PAYMENT_METHOD_ID),
+ )
+ }
+ }
+}
diff --git a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt
index bd40ab9e14..ee97f87ffb 100644
--- a/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt
+++ b/components-core/src/main/java/com/adyen/checkout/components/core/paymentmethod/PaymentMethodDetails.kt
@@ -54,34 +54,38 @@ abstract class PaymentMethodDetails : ModelObject() {
@Suppress("CyclomaticComplexMethod")
fun getChildSerializer(paymentMethodType: String): Serializer {
val serializer = when (paymentMethodType) {
- IdealPaymentMethod.PAYMENT_METHOD_TYPE -> IdealPaymentMethod.SERIALIZER
+ ACHDirectDebitPaymentMethod.PAYMENT_METHOD_TYPE -> ACHDirectDebitPaymentMethod.SERIALIZER
+ BacsDirectDebitPaymentMethod.PAYMENT_METHOD_TYPE -> BacsDirectDebitPaymentMethod.SERIALIZER
+ BlikPaymentMethod.PAYMENT_METHOD_TYPE -> BlikPaymentMethod.SERIALIZER
CardPaymentMethod.PAYMENT_METHOD_TYPE -> CardPaymentMethod.SERIALIZER
- PaymentMethodTypes.MOLPAY_MALAYSIA,
- PaymentMethodTypes.MOLPAY_THAILAND,
- PaymentMethodTypes.MOLPAY_VIETNAM -> MolpayPaymentMethod.SERIALIZER
+ CashAppPayPaymentMethod.PAYMENT_METHOD_TYPE -> CashAppPayPaymentMethod.SERIALIZER
+ ConvenienceStoresJPPaymentMethod.PAYMENT_METHOD_TYPE -> ConvenienceStoresJPPaymentMethod.SERIALIZER
DotpayPaymentMethod.PAYMENT_METHOD_TYPE -> DotpayPaymentMethod.SERIALIZER
- OnlineBankingCZPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingCZPaymentMethod.SERIALIZER
- OnlineBankingPLPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingPLPaymentMethod.SERIALIZER
- OnlineBankingSKPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingSKPaymentMethod.SERIALIZER
EPSPaymentMethod.PAYMENT_METHOD_TYPE -> EPSPaymentMethod.SERIALIZER
- OpenBankingPaymentMethod.PAYMENT_METHOD_TYPE -> OpenBankingPaymentMethod.SERIALIZER
EntercashPaymentMethod.PAYMENT_METHOD_TYPE -> EntercashPaymentMethod.SERIALIZER
GiftCardPaymentMethod.PAYMENT_METHOD_TYPE -> GiftCardPaymentMethod.SERIALIZER
- PaymentMethodTypes.GOOGLE_PAY,
- PaymentMethodTypes.GOOGLE_PAY_LEGACY -> GooglePayPaymentMethod.SERIALIZER
- SepaPaymentMethod.PAYMENT_METHOD_TYPE -> SepaPaymentMethod.SERIALIZER
+ IdealPaymentMethod.PAYMENT_METHOD_TYPE -> IdealPaymentMethod.SERIALIZER
MBWayPaymentMethod.PAYMENT_METHOD_TYPE -> MBWayPaymentMethod.SERIALIZER
- BlikPaymentMethod.PAYMENT_METHOD_TYPE -> BlikPaymentMethod.SERIALIZER
- BacsDirectDebitPaymentMethod.PAYMENT_METHOD_TYPE -> BacsDirectDebitPaymentMethod.SERIALIZER
- PayByBankPaymentMethod.PAYMENT_METHOD_TYPE -> PayByBankPaymentMethod.SERIALIZER
- ConvenienceStoresJPPaymentMethod.PAYMENT_METHOD_TYPE -> ConvenienceStoresJPPaymentMethod.SERIALIZER
+ OnlineBankingCZPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingCZPaymentMethod.SERIALIZER
OnlineBankingJPPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingJPPaymentMethod.SERIALIZER
+ OnlineBankingPLPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingPLPaymentMethod.SERIALIZER
+ OnlineBankingSKPaymentMethod.PAYMENT_METHOD_TYPE -> OnlineBankingSKPaymentMethod.SERIALIZER
+ OpenBankingPaymentMethod.PAYMENT_METHOD_TYPE -> OpenBankingPaymentMethod.SERIALIZER
+ PayByBankPaymentMethod.PAYMENT_METHOD_TYPE -> PayByBankPaymentMethod.SERIALIZER
PayEasyPaymentMethod.PAYMENT_METHOD_TYPE -> PayEasyPaymentMethod.SERIALIZER
- SevenElevenPaymentMethod.PAYMENT_METHOD_TYPE -> SevenElevenPaymentMethod.SERIALIZER
- ACHDirectDebitPaymentMethod.PAYMENT_METHOD_TYPE -> ACHDirectDebitPaymentMethod.SERIALIZER
+ PaymentMethodTypes.GOOGLE_PAY,
+ PaymentMethodTypes.GOOGLE_PAY_LEGACY -> GooglePayPaymentMethod.SERIALIZER
+
+ PaymentMethodTypes.MOLPAY_MALAYSIA,
+ PaymentMethodTypes.MOLPAY_THAILAND,
+ PaymentMethodTypes.MOLPAY_VIETNAM -> MolpayPaymentMethod.SERIALIZER
+
PaymentMethodTypes.UPI,
PaymentMethodTypes.UPI_COLLECT,
PaymentMethodTypes.UPI_QR -> UPIPaymentMethod.SERIALIZER
+
+ SepaPaymentMethod.PAYMENT_METHOD_TYPE -> SepaPaymentMethod.SERIALIZER
+ SevenElevenPaymentMethod.PAYMENT_METHOD_TYPE -> SevenElevenPaymentMethod.SERIALIZER
else -> GenericPaymentMethod.SERIALIZER
}
@Suppress("UNCHECKED_CAST")
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 7328a957d9..638a940b5b 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
@@ -104,7 +104,7 @@ internal class AnalyticsMapperTest {
val expected = mapOf(
"payload_version" to "1",
- "version" to "5.0.0-alpha01",
+ "version" to "5.0.0-alpha02",
"flavor" to "components",
"component" to "PAYMENT_METHOD_TYPE",
"locale" to "en_US",
diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultStatusRepositoryTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultStatusRepositoryTest.kt
index ad02e29fde..0c924c7721 100644
--- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultStatusRepositoryTest.kt
+++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/data/api/DefaultStatusRepositoryTest.kt
@@ -53,7 +53,7 @@ internal class DefaultStatusRepositoryTest(
}
@Test
- fun `when refreshing the status, then the result is emitted immediately`() = runTest() {
+ fun `when refreshing the status, then the result is emitted immediately`() = runTest {
val refreshResponse = StatusResponse(resultCode = "refresh")
whenever(statusService.checkStatus(any(), any()))
// return final result first, so polling stops
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 6c95521d5e..cf8b0c6368 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
@@ -83,7 +83,8 @@ internal class ButtonComponentParamsMapperTest {
sessionParams = SessionParams(
enableStoreDetails = null,
installmentOptions = null,
- amount = sessionsValue
+ amount = sessionsValue,
+ returnUrl = "",
)
)
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 d641fb40d4..3065f30bf4 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
@@ -94,7 +94,8 @@ internal class GenericComponentParamsMapperTest {
sessionParams = SessionParams(
enableStoreDetails = null,
installmentOptions = null,
- amount = sessionsValue
+ amount = sessionsValue,
+ returnUrl = "",
)
)
diff --git a/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/ValidationUtilsTest.kt b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/ValidationUtilsTest.kt
index f0989dc47c..97d4633fc2 100644
--- a/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/ValidationUtilsTest.kt
+++ b/components-core/src/test/java/com/adyen/checkout/components/core/internal/util/ValidationUtilsTest.kt
@@ -80,13 +80,13 @@ internal class ValidationUtilsTest {
arguments("user-@example.org", true),
arguments("postmaster@[123.123.123.123]", true),
arguments("john.smith@mohamed12.eldoheiri", true),
- arguments("john.smith@[12.2.344.45]", true),
+ arguments("john.smith@[12.2.255.45]", true),
- arguments("john.smith!#$%&'*+-/=?^_`{|}~@[12.2.344.45]", true),
- arguments("john!#$%&'*+-/=?^_`{|}~.smith@[12.2.344.45]", true),
+ arguments("john.smith!#$%&'*+-/=?^_`{|}~@[12.2.255.45]", true),
+ arguments("john!#$%&'*+-/=?^_`{|}~.smith@[12.2.255.45]", true),
arguments(
"john!#$%&'*+-/=?^_`{|}~.smith!#$%&'*+-/=?^_`{|}~" +
- ".efwe!#$%&'*+-/=?^_`{|}~.weoihefw.!#$%&'*+-/=?^_`{|}~@[12.2.344.45]",
+ ".efwe!#$%&'*+-/=?^_`{|}~.weoihefw.!#$%&'*+-/=?^_`{|}~@[12.2.255.45]",
true
),
arguments("\" ewc429 (%($^)*_)*(&&R%$&$&^$# \"@mohamed12.eldoheiri", true),
@@ -137,6 +137,9 @@ internal class ValidationUtilsTest {
// Domain part can be an IP address of four of 1-3 long numbers separated by a dot.
arguments("john.smith@[12.2.344.45].com", false),
+ // The IP address is out of bounds
+ arguments("john.smith@[12.2.344.45]", false),
+
// The Domain part is not a valid IP address.
arguments("john.smith@[12.2.344]", false),
diff --git a/config/detekt/detekt-baseline.xml b/config/detekt/detekt-baseline.xml
index c1902a7cd7..c59c4cf892 100644
--- a/config/detekt/detekt-baseline.xml
+++ b/config/detekt/detekt-baseline.xml
@@ -11,5 +11,14 @@
+ ForbiddenComment:Logger.kt$Logger$// 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
+ ForbiddenComment:CardView.kt$CardView$// TODO: 29/01/2021 get this logic from OutputData
+ ForbiddenComment:VoucherAction.kt$VoucherAction$// TODO: remove url when it's fixed from backend side
+ ForbiddenComment:ActionComponentDialogFragment.kt$ActionComponentDialogFragment$// TODO: trigger download image flow when user accept storage permission after checking permission type
+ ForbiddenComment:ActionComponentDialogFragment.kt$ActionComponentDialogFragment$// TODO: checkout_rationale_title_storage_permission and checkout_rationale_message_storage_permission
+ ForbiddenComment:ActionComponentDialogFragment.kt$ActionComponentDialogFragment$// TODO: can be reused based on required permission
+ ForbiddenComment:DefaultVoucherDelegate.kt$DefaultVoucherDelegate$// TODO: remove action.url when it's fixed from backend side
+ ForbiddenComment:CheckoutSessionInitializer.kt$CheckoutSessionInitializer$// TODO: Once Backend provides the correct amount in the SessionSetupResponse use that in SessionDetails instead of
+ ForbiddenComment:SessionDetails.kt$// TODO: Once Backend provides the correct amount in the SessionSetupResponse use that in SessionDetails
-
\ No newline at end of file
+
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
index 5d4f07b4b4..b38822b2a8 100644
--- a/config/detekt/detekt.yml
+++ b/config/detekt/detekt.yml
@@ -1,594 +1,21 @@
-build:
- maxIssues: 0
- excludeCorrectable: false
- weights:
- # complexity: 2
- # LongParameterList: 1
- # style: 1
- # comments: 1
-
-config:
- validation: true
- warningsAsErrors: false
- checkExhaustiveness: false
- # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
- excludes: ''
-
-processors:
- active: true
- exclude:
- - 'DetektProgressListener'
- # - 'KtFileCountProcessor'
- # - 'PackageCountProcessor'
- # - 'ClassCountProcessor'
- # - 'FunctionCountProcessor'
- # - 'PropertyCountProcessor'
- # - 'ProjectComplexityProcessor'
- # - 'ProjectCognitiveComplexityProcessor'
- # - 'ProjectLLOCProcessor'
- # - 'ProjectCLOCProcessor'
- # - 'ProjectLOCProcessor'
- # - 'ProjectSLOCProcessor'
- # - 'LicenseHeaderLoaderExtension'
-
-console-reports:
- active: true
- exclude:
- - 'ProjectStatisticsReport'
- - 'ComplexityReport'
- - 'NotificationReport'
- - 'FindingsReport'
- - 'FileBasedFindingsReport'
- # - 'LiteFindingsReport'
-
-output-reports:
- active: true
- exclude:
- # - 'TxtOutputReport'
- # - 'XmlOutputReport'
- # - 'HtmlOutputReport'
- # - 'MdOutputReport'
-
-comments:
- active: true
- AbsentOrWrongFileLicense:
- active: false
- licenseTemplateFile: 'license.template'
- licenseTemplateIsRegex: false
- CommentOverPrivateFunction:
- active: false
- CommentOverPrivateProperty:
- active: false
- DeprecatedBlockTag:
- active: false
- EndOfSentenceFormat:
- active: false
- endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
- KDocReferencesNonPublicProperty:
- active: false
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- OutdatedDocumentation:
- active: false
- matchTypeParameters: true
- matchDeclarationsOrder: true
- allowParamOnConstructorProperties: false
- UndocumentedPublicClass:
- active: false
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- searchInNestedClass: true
- searchInInnerClass: true
- searchInInnerObject: true
- searchInInnerInterface: true
- searchInProtectedClass: false
- UndocumentedPublicFunction:
- active: false
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- searchProtectedFunction: false
- UndocumentedPublicProperty:
- active: false
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- searchProtectedProperty: false
-
complexity:
active: true
- CognitiveComplexMethod:
- active: false
- threshold: 15
- ComplexCondition:
- active: true
- threshold: 4
- ComplexInterface:
- active: false
- threshold: 10
- includeStaticDeclarations: false
- includePrivateDeclarations: false
- ignoreOverloaded: false
- CyclomaticComplexMethod:
- active: true
- threshold: 15
- ignoreSingleWhenExpression: false
- ignoreSimpleWhenEntries: false
- ignoreNestingFunctions: false
- nestingFunctions:
- - 'also'
- - 'apply'
- - 'forEach'
- - 'isNotNull'
- - 'ifNull'
- - 'let'
- - 'run'
- - 'use'
- - 'with'
- LabeledExpression:
- active: false
- ignoredLabels: []
- LargeClass:
- active: true
- threshold: 600
LongMethod:
active: true
threshold: 60
excludes:
- '**/test/**'
- '**/androidTest/**'
- LongParameterList:
- active: true
- functionThreshold: 6
- constructorThreshold: 7
- ignoreDefaultParameters: false
- ignoreDataClasses: true
- ignoreAnnotatedParameter: []
- MethodOverloading:
- active: false
- threshold: 6
- NamedArguments:
- active: false
- threshold: 3
- ignoreArgumentsMatchingNames: false
- NestedBlockDepth:
- active: true
- threshold: 4
- NestedScopeFunctions:
- active: false
- threshold: 1
- functions:
- - 'kotlin.apply'
- - 'kotlin.run'
- - 'kotlin.with'
- - 'kotlin.let'
- - 'kotlin.also'
- ReplaceSafeCallChainWithRun:
- active: false
- StringLiteralDuplication:
- active: false
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- threshold: 3
- ignoreAnnotation: true
- excludeStringsWithLessThan5Characters: true
- ignoreStringsRegex: '$^'
- TooManyFunctions:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- thresholdInFiles: 11
- thresholdInClasses: 11
- thresholdInInterfaces: 11
- thresholdInObjects: 11
- thresholdInEnums: 11
- ignoreDeprecated: false
- ignorePrivate: false
- ignoreOverridden: false
-
-coroutines:
- active: true
- GlobalCoroutineUsage:
- active: false
- InjectDispatcher:
- active: true
- dispatcherNames:
- - 'IO'
- - 'Default'
- - 'Unconfined'
- RedundantSuspendModifier:
- active: true
- SleepInsteadOfDelay:
- active: true
- SuspendFunWithCoroutineScopeReceiver:
- active: false
- SuspendFunWithFlowReturnType:
- active: true
-
-empty-blocks:
- active: true
- EmptyCatchBlock:
- active: true
- allowedExceptionNameRegex: '_|(ignore|expected).*'
- EmptyClassBlock:
- active: true
- EmptyDefaultConstructor:
- active: true
- EmptyDoWhileBlock:
- active: true
- EmptyElseBlock:
- active: true
- EmptyFinallyBlock:
- active: true
- EmptyForBlock:
- active: true
- EmptyFunctionBlock:
- active: true
- ignoreOverridden: false
- EmptyIfBlock:
- active: true
- EmptyInitBlock:
- active: true
- EmptyKtFile:
- active: true
- EmptySecondaryConstructor:
- active: true
- EmptyTryBlock:
- active: true
- EmptyWhenBlock:
- active: true
- EmptyWhileBlock:
- active: true
-
-exceptions:
- active: true
- ExceptionRaisedInUnexpectedLocation:
- active: true
- methodNames:
- - 'equals'
- - 'finalize'
- - 'hashCode'
- - 'toString'
- InstanceOfCheckForException:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- NotImplementedDeclaration:
- active: false
- ObjectExtendsThrowable:
- active: false
- PrintStackTrace:
- active: true
- RethrowCaughtException:
- active: true
- ReturnFromFinally:
- active: true
- ignoreLabeled: false
- SwallowedException:
- active: true
- ignoredExceptionTypes:
- - 'InterruptedException'
- - 'MalformedURLException'
- - 'NumberFormatException'
- - 'ParseException'
- allowedExceptionNameRegex: '_|(ignore|expected).*'
- ThrowingExceptionFromFinally:
- active: true
- ThrowingExceptionInMain:
- active: false
- ThrowingExceptionsWithoutMessageOrCause:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- exceptions:
- - 'ArrayIndexOutOfBoundsException'
- - 'Exception'
- - 'IllegalArgumentException'
- - 'IllegalMonitorStateException'
- - 'IllegalStateException'
- - 'IndexOutOfBoundsException'
- - 'NullPointerException'
- - 'RuntimeException'
- - 'Throwable'
- ThrowingNewInstanceOfSameException:
- active: true
- TooGenericExceptionCaught:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- exceptionNames:
- - 'ArrayIndexOutOfBoundsException'
- - 'Error'
- - 'Exception'
- - 'IllegalMonitorStateException'
- - 'IndexOutOfBoundsException'
- - 'NullPointerException'
- - 'RuntimeException'
- - 'Throwable'
- allowedExceptionNameRegex: '_|(ignore|expected).*'
- TooGenericExceptionThrown:
- active: true
- exceptionNames:
- - 'Error'
- - 'Exception'
- - 'RuntimeException'
- - 'Throwable'
naming:
active: true
- BooleanPropertyNaming:
- active: false
- allowedPattern: '^(is|has|are)'
- ignoreOverridden: true
- ClassNaming:
- active: true
- classPattern: '[A-Z][a-zA-Z0-9]*'
- ConstructorParameterNaming:
- active: true
- parameterPattern: '[a-z][A-Za-z0-9]*'
- privateParameterPattern: '[a-z][A-Za-z0-9]*'
- excludeClassPattern: '$^'
- ignoreOverridden: true
- EnumNaming:
- active: true
- enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
- ForbiddenClassName:
- active: false
- forbiddenName: []
- FunctionMaxLength:
- active: false
- maximumFunctionNameLength: 30
- FunctionMinLength:
- active: false
- minimumFunctionNameLength: 3
FunctionNaming:
active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- functionPattern: '[a-z][a-zA-Z0-9]*'
- excludeClassPattern: '$^'
- ignoreOverridden: true
- FunctionParameterNaming:
- active: true
- parameterPattern: '[a-z][A-Za-z0-9]*'
- excludeClassPattern: '$^'
- ignoreOverridden: true
- InvalidPackageDeclaration:
- active: true
- rootPackage: ''
- requireRootInDeclaration: false
- LambdaParameterNaming:
- active: false
- parameterPattern: '[a-z][A-Za-z0-9]*|_'
- MatchingDeclarationName:
- active: true
- mustBeFirst: true
- MemberNameEqualsClassName:
- active: true
- ignoreOverridden: true
- NoNameShadowing:
- active: true
- NonBooleanPropertyPrefixedWithIs:
- active: false
- ObjectPropertyNaming:
- active: true
- constantPattern: '[A-Za-z][_A-Za-z0-9]*'
- propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
- privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
- PackageNaming:
- active: true
- packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
- TopLevelPropertyNaming:
- active: true
- constantPattern: '[A-Z][_A-Z0-9]*'
- propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
- privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
- VariableMaxLength:
- active: false
- maximumVariableNameLength: 64
- VariableMinLength:
- active: false
- minimumVariableNameLength: 1
- VariableNaming:
- active: true
- variablePattern: '[a-z][A-Za-z0-9]*'
- privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
- excludeClassPattern: '$^'
- ignoreOverridden: true
-
-performance:
- active: true
- ArrayPrimitive:
- active: true
- CouldBeSequence:
- active: false
- threshold: 3
- ForEachOnRange:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- SpreadOperator:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- UnnecessaryPartOfBinaryExpression:
- active: false
- UnnecessaryTemporaryInstantiation:
- active: true
-
-potential-bugs:
- active: true
- AvoidReferentialEquality:
- active: true
- forbiddenTypePatterns:
- - 'kotlin.String'
- CastToNullableType:
- active: false
- Deprecation:
- active: false
- DontDowncastCollectionTypes:
- active: false
- DoubleMutabilityForCollection:
- active: true
- mutableTypes:
- - 'kotlin.collections.MutableList'
- - 'kotlin.collections.MutableMap'
- - 'kotlin.collections.MutableSet'
- - 'java.util.ArrayList'
- - 'java.util.LinkedHashSet'
- - 'java.util.HashSet'
- - 'java.util.LinkedHashMap'
- - 'java.util.HashMap'
- ElseCaseInsteadOfExhaustiveWhen:
- active: false
- EqualsAlwaysReturnsTrueOrFalse:
- active: true
- EqualsWithHashCodeExist:
- active: true
- ExitOutsideMain:
- active: false
- ExplicitGarbageCollectionCall:
- active: true
- HasPlatformType:
- active: true
- IgnoredReturnValue:
- active: true
- restrictToConfig: true
- returnValueAnnotations:
- - '*.CheckResult'
- - '*.CheckReturnValue'
- ignoreReturnValueAnnotations:
- - '*.CanIgnoreReturnValue'
- returnValueTypes:
- - 'kotlin.sequences.Sequence'
- - 'kotlinx.coroutines.flow.*Flow'
- - 'java.util.stream.*Stream'
- ignoreFunctionCall: []
- ImplicitDefaultLocale:
- active: false
- ImplicitUnitReturnType:
- active: false
- allowExplicitReturnType: true
- InvalidRange:
- active: true
- IteratorHasNextCallsNextMethod:
- active: true
- IteratorNotThrowingNoSuchElementException:
- active: true
- LateinitUsage:
- active: false
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- ignoreOnClassesPattern: ''
- MapGetWithNotNullAssertionOperator:
- active: true
- MissingPackageDeclaration:
- active: false
- excludes: ['**/*.kts']
- NullCheckOnMutableProperty:
- active: false
- NullableToStringCall:
- active: false
- UnconditionalJumpStatementInLoop:
- active: false
- UnnecessaryNotNullCheck:
- active: false
- UnnecessaryNotNullOperator:
- active: true
- UnnecessarySafeCall:
- active: true
- UnreachableCatchBlock:
- active: true
- UnreachableCode:
- active: true
- UnsafeCallOnNullableType:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
- UnsafeCast:
- active: true
- UnusedUnaryOperator:
- active: true
- UselessPostfixExpression:
- active: true
- WrongEqualsTypeParameter:
- active: true
+ ignoreAnnotated:
+ - 'Composable'
style:
active: true
- AlsoCouldBeApply:
- active: false
- CanBeNonNullable:
- active: false
- CascadingCallWrapping:
- active: false
- includeElvis: true
- ClassOrdering:
- active: false
- CollapsibleIfStatements:
- active: false
- DataClassContainsFunctions:
- active: false
- conversionFunctionPrefix:
- - 'to'
- DataClassShouldBeImmutable:
- active: false
- DestructuringDeclarationWithTooManyEntries:
- active: true
- maxDestructuringEntries: 3
- EqualsNullCall:
- active: true
- EqualsOnSignatureLine:
- active: false
- ExplicitCollectionElementAccessMethod:
- active: false
- ExplicitItLambdaParameter:
- active: true
- ExpressionBodySyntax:
- active: false
- includeLineWrapping: false
- ForbiddenComment:
- active: true
- values:
- - 'FIXME:'
- - 'STOPSHIP:'
- allowedPatterns: ''
- customMessage: ''
- ForbiddenImport:
- active: false
- imports: []
- forbiddenPatterns: ''
- ForbiddenMethodCall:
- active: false
- methods:
- - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
- value: 'kotlin.io.print'
- - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
- value: 'kotlin.io.println'
- ForbiddenSuppress:
- active: false
- rules: []
- ForbiddenVoid:
- active: true
- ignoreOverridden: false
- ignoreUsageInGenerics: false
- FunctionOnlyReturningConstant:
- active: true
- ignoreOverridableFunction: true
- ignoreActualFunction: true
- excludedFunctions: []
- LoopWithTooManyJumpStatements:
- active: true
- maxJumpCount: 1
- MagicNumber:
- active: true
- excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
- ignoreNumbers:
- - '-1'
- - '0'
- - '1'
- - '2'
- ignoreHashCodeFunction: true
- ignorePropertyDeclaration: false
- ignoreLocalVariableDeclaration: false
- ignoreConstantDeclaration: true
- ignoreCompanionObjectPropertyDeclaration: true
- ignoreAnnotation: false
- ignoreNamedArgument: true
- ignoreEnums: false
- ignoreRanges: false
- ignoreExtensionFunctions: true
- MandatoryBracesIfStatements:
- active: false
- MandatoryBracesLoops:
- active: false
- MaxChainedCallsOnSameLine:
- active: false
- maxChainedCalls: 5
MaxLineLength:
active: true
maxLineLength: 120
@@ -598,333 +25,12 @@ style:
excludeRawStrings: true
ignoreAnnotated:
- Test
- MayBeConst:
- active: true
- ModifierOrder:
- active: true
- MultilineLambdaItParameter:
- active: false
- MultilineRawStringIndentation:
- active: false
- indentSize: 4
- NestedClassesVisibility:
- active: true
- NewLineAtEndOfFile:
- active: true
- NoTabs:
- active: false
- NullableBooleanCheck:
- active: false
- ObjectLiteralToLambda:
- active: true
- OptionalAbstractKeyword:
- active: true
- OptionalUnit:
- active: false
- OptionalWhenBraces:
- active: false
- PreferToOverPairSyntax:
- active: false
- ProtectedMemberInFinalClass:
- active: true
- RedundantExplicitType:
- active: false
- RedundantHigherOrderMapUsage:
- active: true
- RedundantVisibilityModifierRule:
- active: false
- ReturnCount:
- active: true
- max: 2
- excludedFunctions:
- - 'equals'
- excludeLabeled: false
- excludeReturnFromLambda: true
- excludeGuardClauses: false
- SafeCast:
- active: true
- SerialVersionUIDInSerializableClass:
- active: true
- SpacingBetweenPackageAndImports:
- active: false
- ThrowsCount:
- active: true
- max: 2
- excludeGuardClauses: false
- TrailingWhitespace:
- active: false
- TrimMultilineRawString:
- active: false
- UnderscoresInNumericLiterals:
- active: false
- acceptableLength: 4
- allowNonStandardGrouping: false
- UnnecessaryAbstractClass:
- active: true
- UnnecessaryAnnotationUseSiteTarget:
- active: false
- UnnecessaryApply:
- active: true
- UnnecessaryBackticks:
- active: false
- UnnecessaryFilter:
- active: true
- UnnecessaryInheritance:
- active: true
- UnnecessaryInnerClass:
- active: false
- UnnecessaryLet:
- active: false
- UnnecessaryParentheses:
- active: false
- allowForUnclearPrecedence: false
- UntilInsteadOfRangeTo:
- active: false
- UnusedImports:
- active: false
- UnusedPrivateClass:
- active: true
- UnusedPrivateMember:
- active: true
- allowedNames: '(_|ignored|expected|serialVersionUID)'
- UseAnyOrNoneInsteadOfFind:
- active: true
- UseArrayLiteralsInAnnotations:
- active: true
- UseCheckNotNull:
- active: true
- UseCheckOrError:
- active: false
- UseDataClass:
- active: false
- allowVars: false
- UseEmptyCounterpart:
- active: false
- UseIfEmptyOrIfBlank:
- active: false
- UseIfInsteadOfWhen:
- active: false
- UseIsNullOrEmpty:
- active: true
- UseOrEmpty:
- active: true
- UseRequire:
- active: false
- UseRequireNotNull:
- active: true
- UseSumOfInsteadOfFlatMapSize:
- active: false
- UselessCallOnNotNull:
- active: true
- UtilityClassWithPublicConstructor:
- active: true
- VarCouldBeVal:
- active: true
- ignoreLateinitVar: false
- WildcardImport:
- active: true
- excludeImports:
- - 'java.util.*'
formatting:
active: true
- android: false
+ android: true
autoCorrect: true
- AnnotationOnSeparateLine:
- active: true
- autoCorrect: true
- AnnotationSpacing:
- active: true
- autoCorrect: true
- ArgumentListWrapping:
- active: true
- autoCorrect: true
- indentSize: 4
- maxLineLength: 120
- BlockCommentInitialStarAlignment:
- active: false
- autoCorrect: true
- ChainWrapping:
- active: true
- autoCorrect: true
- CommentSpacing:
- active: true
- autoCorrect: true
- CommentWrapping:
- active: false
- autoCorrect: true
- indentSize: 4
- DiscouragedCommentLocation:
- active: false
- autoCorrect: true
- EnumEntryNameCase:
- active: true
- autoCorrect: true
- Filename:
- active: true
- FinalNewline:
- active: true
- autoCorrect: true
- insertFinalNewLine: true
- FunKeywordSpacing:
- active: false
- autoCorrect: true
- FunctionReturnTypeSpacing:
- active: false
- autoCorrect: true
- FunctionSignature:
- active: false
- autoCorrect: true
- forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647
- functionBodyExpressionWrapping: 'default'
- maxLineLength: 120
- indentSize: 4
- FunctionStartOfBodySpacing:
- active: false
- autoCorrect: true
- FunctionTypeReferenceSpacing:
- active: false
- autoCorrect: true
- ImportOrdering:
- active: true
- autoCorrect: true
- layout: '*,java.**,javax.**,kotlin.**,^'
- Indentation:
- active: true
- autoCorrect: true
- indentSize: 4
- KdocWrapping:
- active: false
- autoCorrect: true
- indentSize: 4
MaximumLineLength:
active: true
maxLineLength: 120
ignoreBackTickedIdentifier: true
- ModifierListSpacing:
- active: false
- autoCorrect: true
- ModifierOrdering:
- active: true
- autoCorrect: true
- MultiLineIfElse:
- active: true
- autoCorrect: true
- NoBlankLineBeforeRbrace:
- active: true
- autoCorrect: true
- NoBlankLinesInChainedMethodCalls:
- active: true
- autoCorrect: true
- NoConsecutiveBlankLines:
- active: true
- autoCorrect: true
- NoEmptyClassBody:
- active: true
- autoCorrect: true
- NoEmptyFirstLineInMethodBlock:
- active: true
- autoCorrect: true
- NoLineBreakAfterElse:
- active: true
- autoCorrect: true
- NoLineBreakBeforeAssignment:
- active: true
- autoCorrect: true
- NoMultipleSpaces:
- active: true
- autoCorrect: true
- NoSemicolons:
- active: true
- autoCorrect: true
- NoTrailingSpaces:
- active: true
- autoCorrect: true
- NoUnitReturn:
- active: true
- autoCorrect: true
- NoUnusedImports:
- active: true
- autoCorrect: true
- NoWildcardImports:
- active: true
- packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**'
- NullableTypeSpacing:
- active: false
- autoCorrect: true
- PackageName:
- active: true
- autoCorrect: true
- ParameterListSpacing:
- active: false
- autoCorrect: true
- ParameterListWrapping:
- active: true
- autoCorrect: true
- maxLineLength: 120
- SpacingAroundAngleBrackets:
- active: true
- autoCorrect: true
- SpacingAroundColon:
- active: true
- autoCorrect: true
- SpacingAroundComma:
- active: true
- autoCorrect: true
- SpacingAroundCurly:
- active: true
- autoCorrect: true
- SpacingAroundDot:
- active: true
- autoCorrect: true
- SpacingAroundDoubleColon:
- active: true
- autoCorrect: true
- SpacingAroundKeyword:
- active: true
- autoCorrect: true
- SpacingAroundOperators:
- active: true
- autoCorrect: true
- SpacingAroundParens:
- active: true
- autoCorrect: true
- SpacingAroundRangeOperator:
- active: true
- autoCorrect: true
- SpacingAroundUnaryOperator:
- active: true
- autoCorrect: true
- SpacingBetweenDeclarationsWithAnnotations:
- active: true
- autoCorrect: true
- SpacingBetweenDeclarationsWithComments:
- active: true
- autoCorrect: true
- SpacingBetweenFunctionNameAndOpeningParenthesis:
- active: false
- autoCorrect: true
- StringTemplate:
- active: true
- autoCorrect: true
- TrailingCommaOnCallSite:
- active: false
- autoCorrect: true
- useTrailingCommaOnCallSite: true
- TrailingCommaOnDeclarationSite:
- active: false
- autoCorrect: true
- useTrailingCommaOnDeclarationSite: true
- TypeArgumentListSpacing:
- active: false
- autoCorrect: true
- TypeParameterListSpacing:
- active: false
- autoCorrect: true
- UnnecessaryParenthesesBeforeTrailingLambda:
- active: false
- autoCorrect: true
- Wrapping:
- active: true
- autoCorrect: true
- indentSize: 4
diff --git a/config/gradle/checksums.gradle b/config/gradle/checksums.gradle
deleted file mode 100644
index e4580c0164..0000000000
--- a/config/gradle/checksums.gradle
+++ /dev/null
@@ -1,49 +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 ran on 6/2/2019.
- */
-
-if (!hasProperty("checksums")) {
- final checksums = [
- // Adyen dependencies
- "com.adyen.threeds:adyen-3ds2:2.2.13:dfa2025daa56db88d9c179cdeb51e143:MD5",
-
- // Kotlin
- "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21:59e5a79996f1d856ddea6533a1080f86:MD5",
- "org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.21:5f1ea09db2f1b0b8fd48b2ed104f0de1:MD5",
- "org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.8.21:58f2a5cc17f02e60cc0014c7c265c463:MD5",
- "org.jetbrains.kotlin:kotlin-parcelize-compiler:1.8.21:acf196a87704e19209aadec25a0a3572:MD5",
- "org.jetbrains.kotlin:kotlin-parcelize-runtime:1.8.21:3fe5cd12466e726fdc65aed02f1339ca:MD5",
-
- // Coroutines
- "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4:7aad79016f327ae9cfd0b560b0b47558:MD5",
-
- // Android Gradle Plugin
- "androidx.databinding:viewbinding:7.4.2:c3d1fbf450fc578ca2d69d6f80645bf4:MD5",
-
- // AndroidX
- "androidx.annotation:annotation:1.5.0:5f8c96f5310d39845efa73dd82b717e4:MD5",
- "androidx.constraintlayout:constraintlayout:2.1.4:c7694aeadf54bc50258e3f1481c797e1:MD5",
- "androidx.fragment:fragment-ktx:1.5.7:eeb80b8fe8b1bd8bf9c41149e69b1dad:MD5",
- "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1:ddec34e06f4a0f22fd183b94a6772fce:MD5",
- "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1:4167b1a439be9f530c5cbf26b590e5cb:MD5",
- "androidx.appcompat:appcompat:1.6.1:ccc362b1e6d93ca72726b05ce1b074f2:MD5",
- "androidx.recyclerview:recyclerview:1.3.0:c327ccfe8a41c074966942ca7271d687:MD5",
- "androidx.browser:browser:1.5.0:6f312f71b7ae84c28c6b9b322ddadbcc:MD5",
-
- // Google
- "com.google.android.material:material:1.8.0:be66d71684f2b388946239a6771f5b0f:MD5",
- "com.google.android.gms:play-services-wallet:19.1.0:3dc5c1194f1bc98ccb98b88633b50b52:MD5",
-
- // WeChatPay
- "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.4:8ae40b8665f98587e716b4593401aef2:MD5",
-
- // OkHttp
- "com.squareup.okhttp3:okhttp:4.11.0:8f53e26319679de3ea22261b1899a99c:MD5"
- ]
-
- ext.checksums = checksums
-}
diff --git a/config/gradle/dependenciesCheck.gradle b/config/gradle/dependenciesCheck.gradle
deleted file mode 100644
index 909929cbe6..0000000000
--- a/config/gradle/dependenciesCheck.gradle
+++ /dev/null
@@ -1,57 +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 ran on 6/2/2019.
- */
-
-import java.security.MessageDigest
-
-afterEvaluate {
- def relevantConfigurations = configurations.all.findAll {
- it.canBeResolved && it.name.contains("Classpath") && !it.name.contains("AndroidTest") && !it.name.contains("UnitTest")
- }
-
- if (relevantConfigurations.size() <= 0) {
- return
- }
-
- logger.lifecycle("\nChecking dependencies credibility:")
-
- relevantConfigurations.each { configuration ->
- configuration.resolvedConfiguration.firstLevelModuleDependencies.each { dependency ->
- final moduleGroup = dependency.moduleGroup.toString()
-
- if (moduleGroup != "com.adyen.checkout" && moduleGroup != rootProject.name) {
- dependency.moduleArtifacts.each { artifact ->
- def (checksum, algorithm) = checksums.findResult {
- final parts = it.split(":")
-
- assert parts.length == 5: "Checksum declaration $it has invalid length"
-
- if (dependency.moduleGroup == parts[0] && dependency.moduleName == parts[1] && dependency.moduleVersion == parts[2]) {
- return [parts[3], parts[4]]
- } else {
- return null
- }
- } ?: [null, null]
-
- assert checksum != null: "Missing checksum declaration for $dependency.name"
- assert algorithm != null: "Missing algorithm declaration for $dependency.name"
-
- MessageDigest md = MessageDigest.getInstance(algorithm)
-
- artifact.file.eachByte 4096, { bytes, size ->
- md.update(bytes, 0, size)
- }
- final calculatedChecksum = md.digest().collect { String.format "%02x", it }.join()
-
- assert checksum == calculatedChecksum: "Checksum ($algorithm) does not match: $dependency.name"
- }
- }
- }
- }
-
- logger.lifecycle("Done successfully.\n")
-}
diff --git a/config/gradle/detekt.gradle b/config/gradle/detekt.gradle
index 805ccdb625..8b220185c7 100644
--- a/config/gradle/detekt.gradle
+++ b/config/gradle/detekt.gradle
@@ -21,6 +21,7 @@ detekt {
config = files("$rootProject.rootDir/config/detekt/detekt.yml")
baseline = file("$rootProject.rootDir/config/detekt/detekt-baseline.xml")
+ buildUponDefaultConfig = true
}
tasks.named("detekt").configure {
diff --git a/config/gradle/release.gradle b/config/gradle/release.gradle
index 344211911b..f0bc33c90d 100644
--- a/config/gradle/release.gradle
+++ b/config/gradle/release.gradle
@@ -19,14 +19,12 @@ ext["sonatypeStagingProfileId"] = ''
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
- logger.lifecycle("\n-- Getting Secrets from local.properties --\n")
Properties p = new Properties()
p.load(new FileInputStream(secretPropsFile))
p.each { name, value ->
ext[name] = value
}
} else {
- logger.lifecycle("\n-- Getting Secrets from System Env --\n")
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
@@ -54,6 +52,8 @@ final theTeamName = "Checkout"
final theScmConnection = "scm:git:git://github.com/Adyen/adyen-android.git"
final theScmUrl = "https://github.com/Adyen/adyen-android"
+group theGroupId
+
project.afterEvaluate {
publishing {
publications {
@@ -149,4 +149,4 @@ project.afterEvaluate {
signing {
sign publishing.publications
}
-}
\ No newline at end of file
+}
diff --git a/config/gradle/sharedTasks.gradle b/config/gradle/sharedTasks.gradle
index 157991c128..84248bbd4a 100644
--- a/config/gradle/sharedTasks.gradle
+++ b/config/gradle/sharedTasks.gradle
@@ -8,9 +8,7 @@
*/
apply from: "${rootDir}/config/gradle/versionName.gradle"
-apply from: "${rootDir}/config/gradle/checksums.gradle"
-apply from: "${rootDir}/config/gradle/dependenciesCheck.gradle"
apply from: "${rootDir}/config/gradle/codeQuality.gradle"
apply from: "${rootDir}/config/gradle/ci.gradle"
apply from: "${rootDir}/config/gradle/release.gradle"
-apply from: "${rootDir}/config/gradle/runConnectedAndroidTest.gradle"
\ No newline at end of file
+apply from: "${rootDir}/config/gradle/runConnectedAndroidTest.gradle"
diff --git a/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPComponent.kt b/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPComponent.kt
index d5401599b7..dab6529e50 100644
--- a/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPComponent.kt
+++ b/convenience-stores-jp/src/main/java/com/adyen/checkout/conveniencestoresjp/ConvenienceStoresJPComponent.kt
@@ -8,8 +8,8 @@
package com.adyen.checkout.conveniencestoresjp
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.PaymentMethodTypes
import com.adyen.checkout.components.core.internal.ComponentEventHandler
import com.adyen.checkout.components.core.internal.PaymentComponent
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 a4bd5a6b25..b0e0c0c445 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
@@ -9,7 +9,7 @@
package com.adyen.checkout.conveniencestoresjp
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
+import com.adyen.checkout.action.core.GenericActionConfiguration
import com.adyen.checkout.components.core.Amount
import com.adyen.checkout.core.Environment
import com.adyen.checkout.econtext.internal.EContextConfiguration
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 abb5d82797..acdf0864ea 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
@@ -9,8 +9,8 @@
package com.adyen.checkout.conveniencestoresjp.internal.provider
import androidx.annotation.RestrictTo
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.PaymentComponentData
import com.adyen.checkout.components.core.internal.ComponentEventHandler
import com.adyen.checkout.components.core.internal.ui.model.ComponentParams
@@ -22,15 +22,17 @@ import com.adyen.checkout.conveniencestoresjp.ConvenienceStoresJPConfiguration
import com.adyen.checkout.econtext.internal.provider.EContextComponentProvider
import com.adyen.checkout.econtext.internal.ui.EContextDelegate
+class ConvenienceStoresJPComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class ConvenienceStoresJPComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) : EContextComponentProvider<
ConvenienceStoresJPComponent,
ConvenienceStoresJPConfiguration,
ConvenienceStoresJPPaymentMethod,
- ConvenienceStoresJPComponentState>(
+ ConvenienceStoresJPComponentState
+ >(
componentClass = ConvenienceStoresJPComponent::class.java,
overrideComponentParams = overrideComponentParams,
overrideSessionParams = overrideSessionParams,
diff --git a/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt b/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt
index c279dab222..f5aa2be473 100644
--- a/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt
+++ b/cse/src/main/java/com/adyen/checkout/cse/internal/ClientSideEncrypter.kt
@@ -104,6 +104,7 @@ class ClientSideEncrypter {
return try {
encryptedAesKey = rsaCipher.doFinal(aesKey.encoded)
String.format(
+ Locale.ROOT,
"%s%s%s%s%s%s",
PREFIX,
VERSION,
diff --git a/dependencies.gradle b/dependencies.gradle
index 3037464b36..b0529971cc 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -16,40 +16,47 @@ 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.0.0-alpha01"
+ version_name = "5.0.0-alpha02"
// Build Script
- android_gradle_plugin_version = '7.4.2'
- kotlin_version = '1.8.21'
- detekt_gradle_plugin_version = "1.22.0"
- dokka_version = "1.8.10"
- hilt_version = "2.46"
+ android_gradle_plugin_version = '8.0.2'
+ kotlin_version = '1.8.22'
+ detekt_gradle_plugin_version = "1.23.0"
+ dokka_version = "1.8.20"
+ hilt_version = "2.46.1"
+ compose_compiler_version = '1.4.8'
// Code quality
- detekt_version = "1.22.0"
- ktlint_version = '0.48.2'
+ detekt_version = "1.23.0"
+ ktlint_version = '0.50.0'
// Android Dependencies
annotation_version = "1.6.0"
appcompat_version = "1.6.1"
browser_version = "1.5.0"
coroutines_version = "1.6.4"
- fragment_version = "1.5.7"
+ fragment_version = "1.6.0"
lifecycle_version = "2.5.1"
- material_version = "1.8.0"
+ material_version = "1.9.0"
recyclerview_version = "1.3.0"
constraintlayout_version = '2.1.4'
+ // Compose Dependencies
+ compose_activity_version = '1.7.2'
+ compose_bom_version = '2023.06.01'
+ compose_viewmodel_version = '2.6.1'
+
// Adyen Dependencies
adyen3ds2_version = "2.2.13"
// External Dependencies
+ cash_app_pay_version = '2.2.0'
okhttp_version = "4.11.0"
- play_services_wallet_version = '19.1.0'
+ play_services_wallet_version = '19.2.0'
wechat_pay_version = "6.6.4"
// Example app
- leak_canary_version = '2.10'
+ leak_canary_version = '2.12'
moshi_adapters_version = '1.14.0'
moshi_kotlin_adapter_version = '1.14.0'
okhttp_logging_version = "4.11.0"
@@ -59,11 +66,11 @@ ext {
// Tests
arch_core_testing_version = "2.2.0"
espresso_version = "3.5.0"
- json_version = '20230227'
+ json_version = '20230618'
junit_jupiter_version = "5.9.1"
mockito_kotlin_version = "4.1.0"
mockito_version = "4.9.0"
- robolectric_version = "4.10"
+ robolectric_version = "4.10.3"
test_ext_version = "1.1.4"
test_rules_version = "1.5.0"
turbine_version = "0.12.1"
@@ -84,6 +91,12 @@ ext {
preference : "androidx.preference:preference-ktx:$preference_version",
recyclerview : "androidx.recyclerview:recyclerview:$recyclerview_version"
],
+ cashAppPay : "app.cash.paykit:core:$cash_app_pay_version",
+ compose : [
+ activity : "androidx.activity:activity-compose:$compose_activity_version",
+ bom : "androidx.compose:compose-bom:$compose_bom_version",
+ viewmodel: "androidx.lifecycle:lifecycle-viewmodel-compose:$compose_viewmodel_version"
+ ],
hilt : "com.google.dagger:hilt-android:$hilt_version",
hiltCompiler : "com.google.dagger:hilt-compiler:$hilt_version",
kotlinCoroutines : [
diff --git a/dotpay/build.gradle b/dotpay/build.gradle
index 0b7bbf6bb1..3c38341e1e 100644
--- a/dotpay/build.gradle
+++ b/dotpay/build.gradle
@@ -36,7 +36,7 @@ android {
dependencies {
// Checkout
- api project(':action')
+ api project(':action-core')
api project(':issuer-list')
// Dependencies
diff --git a/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayComponent.kt b/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayComponent.kt
index 4f60553d7e..972bf85e37 100644
--- a/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayComponent.kt
+++ b/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayComponent.kt
@@ -7,8 +7,8 @@
*/
package com.adyen.checkout.dotpay
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.PaymentMethodTypes
import com.adyen.checkout.components.core.internal.ComponentEventHandler
import com.adyen.checkout.components.core.internal.PaymentComponent
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 5a1fdf0865..c873e86f80 100644
--- a/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayConfiguration.kt
+++ b/dotpay/src/main/java/com/adyen/checkout/dotpay/DotpayConfiguration.kt
@@ -8,7 +8,7 @@
package com.adyen.checkout.dotpay
import android.content.Context
-import com.adyen.checkout.action.GenericActionConfiguration
+import com.adyen.checkout.action.core.GenericActionConfiguration
import com.adyen.checkout.components.core.Amount
import com.adyen.checkout.core.Environment
import com.adyen.checkout.issuerlist.IssuerListViewType
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 5e441bd007..54f0f3ed1a 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
@@ -9,8 +9,8 @@
package com.adyen.checkout.dotpay.internal.provider
import androidx.annotation.RestrictTo
-import com.adyen.checkout.action.internal.DefaultActionHandlingComponent
-import com.adyen.checkout.action.internal.ui.GenericActionDelegate
+import com.adyen.checkout.action.core.internal.DefaultActionHandlingComponent
+import com.adyen.checkout.action.core.internal.ui.GenericActionDelegate
import com.adyen.checkout.components.core.PaymentComponentData
import com.adyen.checkout.components.core.internal.ComponentEventHandler
import com.adyen.checkout.components.core.internal.ui.model.ComponentParams
@@ -22,8 +22,9 @@ import com.adyen.checkout.dotpay.DotpayConfiguration
import com.adyen.checkout.issuerlist.internal.provider.IssuerListComponentProvider
import com.adyen.checkout.issuerlist.internal.ui.IssuerListDelegate
+class DotpayComponentProvider
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-class DotpayComponentProvider(
+constructor(
overrideComponentParams: ComponentParams? = null,
overrideSessionParams: SessionParams? = null,
) : IssuerListComponentProvider(
diff --git a/drop-in-compose/build.gradle b/drop-in-compose/build.gradle
new file mode 100644
index 0000000000..6cefd510b4
--- /dev/null
+++ b/drop-in-compose/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * 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 1/6/2023.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+// Maven artifact
+ext.mavenArtifactId = "drop-in-compose"
+ext.mavenArtifactName = "Adyen checkout drop-in component compose"
+ext.mavenArtifactDescription = "Compose compat Adyen checkout drop-in component."
+
+apply from: "${rootDir}/config/gradle/sharedTasks.gradle"
+
+android {
+ namespace 'com.adyen.checkout.dropin.compose'
+ compileSdkVersion compile_sdk_version
+
+ defaultConfig {
+ minSdkVersion min_sdk_version
+ targetSdkVersion target_sdk_version
+ versionCode version_code
+ versionName version_name
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = compose_compiler_version
+ }
+}
+
+dependencies {
+ // Checkout
+ api project(':drop-in')
+
+ implementation platform(libraries.compose.bom)
+ implementation libraries.compose.activity
+}
diff --git a/drop-in-compose/consumer-rules.pro b/drop-in-compose/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/drop-in-compose/src/main/AndroidManifest.xml b/drop-in-compose/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..c62a023f8b
--- /dev/null
+++ b/drop-in-compose/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
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
new file mode 100644
index 0000000000..3f1fa02bc9
--- /dev/null
+++ b/drop-in-compose/src/main/java/com/adyen/checkout/dropin/compose/ComposeExtensions.kt
@@ -0,0 +1,155 @@
+/*
+ * 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 1/6/2023.
+ */
+
+package com.adyen.checkout.dropin.compose
+
+import android.annotation.SuppressLint
+import androidx.activity.compose.rememberLauncherForActivityResult
+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.PaymentMethodsApiResponse
+import com.adyen.checkout.dropin.DropIn
+import com.adyen.checkout.dropin.DropIn.registerForDropInResult
+import com.adyen.checkout.dropin.DropInCallback
+import com.adyen.checkout.dropin.DropInConfiguration
+import com.adyen.checkout.dropin.DropInResult
+import com.adyen.checkout.dropin.DropInResultContract
+import com.adyen.checkout.dropin.DropInService
+import com.adyen.checkout.dropin.SessionDropInCallback
+import com.adyen.checkout.dropin.SessionDropInResult
+import com.adyen.checkout.dropin.SessionDropInResultContract
+import com.adyen.checkout.dropin.SessionDropInService
+import com.adyen.checkout.dropin.internal.ui.model.DropInResultContractParams
+import com.adyen.checkout.dropin.internal.ui.model.SessionDropInResultContractParams
+import com.adyen.checkout.sessions.core.CheckoutSession
+import com.adyen.checkout.sessions.core.CheckoutSessionProvider
+
+/**
+ * Register your [Composable] with the Activity Result API and receive the final Drop-in result using the
+ * [SessionDropInCallback].
+ *
+ * This *must* be called unconditionally, as part of the initialization path.
+ *
+ * You will receive the Drop-in result in the [SessionDropInCallback.onDropInResult] method. Check out
+ * [SessionDropInResult] class for all the possible results you might receive.
+ *
+ * @param callback Callback for the Drop-in result.
+ *
+ * @return The [ActivityResultLauncher] required to receive the result of Drop-in.
+ */
+@Suppress("unused")
+@Composable
+fun rememberLauncherForDropInResult(
+ callback: SessionDropInCallback
+): ActivityResultLauncher {
+ return rememberLauncherForActivityResult(
+ contract = SessionDropInResultContract(),
+ onResult = callback::onDropInResult
+ )
+}
+
+/**
+ * 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 [dropInConfiguration] 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 dropInConfiguration 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,
+ dropInConfiguration: DropInConfiguration,
+ serviceClass: Class = SessionDropInService::class.java,
+) {
+ val currentContext = LocalContext.current
+ LaunchedEffect(Unit) {
+ startPayment(
+ context = currentContext,
+ dropInLauncher = dropInLauncher,
+ checkoutSession = checkoutSession,
+ dropInConfiguration = dropInConfiguration,
+ serviceClass = serviceClass
+ )
+ }
+}
+
+/**
+ * Register your [Composable] with the Activity Result API and receive the final Drop-in result using the
+ * [DropInCallback].
+ *
+ * This *must* be called unconditionally, as part of the initialization path.
+ *
+ * You will receive the Drop-in result in the [DropInCallback.onDropInResult] method. Check out [DropInResult] for
+ * all the possible results you might receive.
+ *
+ * @param callback Callback for the Drop-in result.
+ *
+ * @return The [ActivityResultLauncher] required to receive the result of Drop-in.
+ */
+@Suppress("unused")
+@Composable
+fun rememberLauncherForDropInResult(
+ callback: DropInCallback
+): ActivityResultLauncher {
+ return rememberLauncherForActivityResult(
+ contract = DropInResultContract(),
+ onResult = callback::onDropInResult
+ )
+}
+
+/**
+ * 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 [dropInConfiguration] 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 dropInConfiguration 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,
+ dropInConfiguration: DropInConfiguration,
+ serviceClass: Class,
+) {
+ val currentContext = LocalContext.current
+ LaunchedEffect(Unit) {
+ startPayment(
+ context = currentContext,
+ dropInLauncher = dropInLauncher,
+ paymentMethodsApiResponse = paymentMethodsApiResponse,
+ dropInConfiguration = dropInConfiguration,
+ serviceClass = serviceClass
+ )
+ }
+}
diff --git a/drop-in/build.gradle b/drop-in/build.gradle
index 85668e1935..b30a953acc 100644
--- a/drop-in/build.gradle
+++ b/drop-in/build.gradle
@@ -47,12 +47,15 @@ android {
dependencies {
// Checkout
+ api project(':3ds2')
api project(':ach')
- api project(':action')
+ api project(':action-core')
api project(':bacs')
api project(':bcmc')
+ api project(':boleto')
api project(':blik')
api project(':card')
+ api project(':cashapppay')
api project(':convenience-stores-jp')
api project(':dotpay')
api project(':entercash')
@@ -74,6 +77,7 @@ dependencies {
api project(':seven-eleven')
api project(':sessions-core')
api project(':upi')
+ api project(':wechatpay')
// Dependencies
implementation libraries.androidx.recyclerview
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 d0b2cacd45..f4de6d5cfc 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
@@ -19,14 +19,14 @@ interface BaseDropInServiceContract {
* [DropInConfiguration.Builder.setEnableRemovingStoredPaymentMethods] to enable this feature.
*
* In this method you should make the network call to tell your server to make a call to the
- * /Recurring//disable endpoint. This method is called when the user initiates
+ * DELETE /storedPaymentMethods endpoint. This method is called when the user initiates
* removing a stored payment method using the remove button.
*
* We provide [storedPaymentMethod] that contains the id of the stored payment method to be removed
* in the field [StoredPaymentMethod.id].
*
* Asynchronous handling: since this method runs on the main thread, you should make sure the
- * /Recurring//disable call and any other long running operation is made on a background thread.
+ * DELETE /storedPaymentMethods call and any other long running operation is made on a background thread.
*
* Use [sendRecurringResult] to send the final result of this call back to the Drop-in.
*
@@ -73,7 +73,7 @@ interface BaseDropInServiceContract {
fun sendOrderResult(result: OrderDropInServiceResult)
/**
- * Allows sending the result of the /Recurring/ network call.
+ * Allows sending the result of the DELETE /storedPaymentMethods network call.
*
* Call this method with a [RecurringDropInServiceResult] depending on the response of the corresponding network
* call.
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 167a79f3b7..7568b5ab4a 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
@@ -11,12 +11,14 @@ package com.adyen.checkout.dropin
import android.content.Context
import android.os.Bundle
import com.adyen.checkout.ach.ACHDirectDebitConfiguration
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.ActionHandlingPaymentMethodConfigurationBuilder
+import com.adyen.checkout.action.core.GenericActionConfiguration
+import com.adyen.checkout.action.core.internal.ActionHandlingPaymentMethodConfigurationBuilder
import com.adyen.checkout.bacs.BacsDirectDebitConfiguration
import com.adyen.checkout.bcmc.BcmcConfiguration
import com.adyen.checkout.blik.BlikConfiguration
+import com.adyen.checkout.boleto.BoletoConfiguration
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.PaymentMethodTypes
import com.adyen.checkout.components.core.internal.Configuration
@@ -90,12 +92,12 @@ class DropInConfiguration private constructor(
/**
* Create a [DropInConfiguration]
*
- * @param context A context
+ * @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(context: Context, environment: Environment, clientKey: String) : super(
- context,
+ constructor(shopperLocale: Locale, environment: Environment, clientKey: String) : super(
+ shopperLocale,
environment,
clientKey
)
@@ -103,12 +105,12 @@ class DropInConfiguration private constructor(
/**
* Alternative constructor that uses the [context] to fetch the user locale and use it as a shopper locale.
*
- * @param shopperLocale The [Locale] of the shopper.
+ * @param context A context
* @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, environment: Environment, clientKey: String) : super(
- shopperLocale,
+ constructor(context: Context, environment: Environment, clientKey: String) : super(
+ context,
environment,
clientKey
)
@@ -162,6 +164,14 @@ class DropInConfiguration private constructor(
return this
}
+ /**
+ * Add configuration for Cash App Pay payment method.
+ */
+ fun addCashAppPayConfiguration(cashAppPayConfiguration: CashAppPayConfiguration): Builder {
+ availablePaymentConfigs[PaymentMethodTypes.CASH_APP_PAY] = cashAppPayConfiguration
+ return this
+ }
+
/**
* Add configuration for iDeal payment method.
*/
@@ -351,6 +361,20 @@ class DropInConfiguration private constructor(
return this
}
+ /**
+ * Add configuration for Boleto payment method.
+ */
+ fun addBoletoConfiguration(boletoConfiguration: BoletoConfiguration): Builder {
+ availablePaymentConfigs[PaymentMethodTypes.BOLETOBANCARIO] = boletoConfiguration
+ availablePaymentConfigs[PaymentMethodTypes.BOLETOBANCARIO_BANCODOBRASIL] = boletoConfiguration
+ availablePaymentConfigs[PaymentMethodTypes.BOLETOBANCARIO_BRADESCO] = boletoConfiguration
+ availablePaymentConfigs[PaymentMethodTypes.BOLETOBANCARIO_HSBC] = boletoConfiguration
+ availablePaymentConfigs[PaymentMethodTypes.BOLETOBANCARIO_ITAU] = boletoConfiguration
+ availablePaymentConfigs[PaymentMethodTypes.BOLETOBANCARIO_SANTANDER] = boletoConfiguration
+ availablePaymentConfigs[PaymentMethodTypes.BOLETO_PRIMEIRO_PAY] = boletoConfiguration
+ return this
+ }
+
override fun buildInternal(): DropInConfiguration {
return DropInConfiguration(
shopperLocale = shopperLocale,
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 9325487a3c..64f8ca649d 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
@@ -182,5 +182,5 @@ internal sealed class SessionDropInServiceResult : BaseDropInServiceResult() {
override val dismissDropIn: Boolean = false
) : SessionDropInServiceResult(), DropInServiceResultError
- internal class Finished(val result: SessionPaymentResult) : SessionDropInServiceResult()
+ class Finished(val result: SessionPaymentResult) : SessionDropInServiceResult()
}
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
index cb052f04f8..831464369a 100644
--- 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
@@ -11,6 +11,7 @@
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
@@ -28,10 +29,18 @@ 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
@@ -41,6 +50,7 @@ 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.provider.PaymentComponentProvider
@@ -51,6 +61,7 @@ import com.adyen.checkout.conveniencestoresjp.internal.provider.ConvenienceStore
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
@@ -139,11 +150,13 @@ 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
+ dropInConfiguration,
+ context,
)
}
@@ -178,11 +191,19 @@ internal fun getDefaultConfigForPaymentMethod(
environment = environment,
clientKey = clientKey
)
+
CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) -> CardConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
+ CashAppPayComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) -> CashAppPayConfiguration.Builder(
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey
+ )
+
else -> throw CheckoutException(
errorMessage = "Unable to find component configuration for storedPaymentMethod - $storedPaymentMethod"
)
@@ -194,7 +215,8 @@ internal fun getDefaultConfigForPaymentMethod(
@Suppress("LongMethod", "CyclomaticComplexMethod")
internal fun getDefaultConfigForPaymentMethod(
paymentMethod: PaymentMethod,
- dropInConfiguration: DropInConfiguration
+ dropInConfiguration: DropInConfiguration,
+ context: Context,
): T {
val shopperLocale = dropInConfiguration.shopperLocale
val environment = dropInConfiguration.environment
@@ -207,133 +229,171 @@ internal fun getDefaultConfigForPaymentMethod(
environment = environment,
clientKey = clientKey
)
+
BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
BacsDirectDebitConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
BcmcComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> BcmcConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
BlikComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> BlikConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
+ BoletoComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> BoletoConfiguration.Builder(
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey
+ )
+
CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> CardConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
+ CashAppPayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> CashAppPayConfiguration.Builder(
+ shopperLocale = shopperLocale,
+ environment = environment,
+ clientKey = clientKey
+ )
+ .setReturnUrl(CashAppPayComponent.getReturnUrl(context))
+
ConvenienceStoresJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
ConvenienceStoresJPConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
DotpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> DotpayConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
EntercashComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> EntercashConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
EPSComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> EPSConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> GiftCardConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
GooglePayConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
IdealComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> IdealConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
InstantPaymentComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> InstantPaymentConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
MBWayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> MBWayConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
MolpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> MolpayConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
OnlineBankingCZComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
OnlineBankingCZConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
OnlineBankingJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
OnlineBankingJPConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
OnlineBankingPLComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
OnlineBankingPLConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
OnlineBankingSKComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
OnlineBankingSKConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
OpenBankingComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> OpenBankingConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
PayByBankComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> PayByBankConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
PayEasyComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> PayEasyConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
SepaComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> SepaConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
SevenElevenComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> SevenElevenConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey
)
+
UPIComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> UPIConfiguration.Builder(
shopperLocale = shopperLocale,
environment = environment,
clientKey = clientKey,
)
+
else -> throw CheckoutException("Unable to find component configuration for paymentMethod - $paymentMethod")
}
@@ -344,10 +404,11 @@ internal fun getDefaultConfigForPaymentMethod(
private inline fun getConfigurationForPaymentMethodOrNull(
paymentMethod: PaymentMethod,
dropInConfiguration: DropInConfiguration,
+ context: Context,
): T? {
@Suppress("SwallowedException")
return try {
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
} catch (e: CheckoutException) {
null
}
@@ -369,7 +430,7 @@ internal fun checkPaymentMethodAvailability(
val availabilityCheck = getPaymentMethodAvailabilityCheck(dropInConfiguration, type, amount, sessionDetails)
val configuration =
- getConfigurationForPaymentMethodOrNull(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethodOrNull(paymentMethod, dropInConfiguration, application)
availabilityCheck.isAvailable(application, paymentMethod, configuration, callback)
} catch (e: CheckoutException) {
@@ -389,13 +450,22 @@ internal fun getPaymentMethodAvailabilityCheck(
): PaymentMethodAvailabilityCheck {
val dropInParams = dropInConfiguration.mapToParams(amount)
val sessionParams = sessionDetails?.mapToParams(amount)
+
@Suppress("UNCHECKED_CAST")
- return when (paymentMethodType) {
+ val availabilityCheck = when (paymentMethodType) {
PaymentMethodTypes.GOOGLE_PAY,
- PaymentMethodTypes.GOOGLE_PAY_LEGACY -> GooglePayComponentProvider(dropInParams, sessionParams)
- PaymentMethodTypes.WECHAT_PAY_SDK -> WeChatPayProvider()
+ PaymentMethodTypes.GOOGLE_PAY_LEGACY -> runCompileOnly {
+ GooglePayComponentProvider(
+ dropInParams,
+ sessionParams
+ )
+ }
+
+ PaymentMethodTypes.WECHAT_PAY_SDK -> runCompileOnly { WeChatPayProvider() }
else -> AlwaysAvailablePaymentMethod()
- } as PaymentMethodAvailabilityCheck
+ } as? PaymentMethodAvailabilityCheck
+
+ return availabilityCheck ?: NotAvailablePaymentMethod()
}
/**
@@ -428,6 +498,7 @@ internal fun getComponentFor(
key = storedPaymentMethod.id
)
}
+
CardComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) -> {
val cardConfig: CardConfiguration =
getConfigurationForPaymentMethod(storedPaymentMethod, dropInConfiguration)
@@ -439,6 +510,19 @@ internal fun getComponentFor(
key = storedPaymentMethod.id
)
}
+
+ 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
+ )
+ }
+
BlikComponent.PROVIDER.isPaymentMethodSupported(storedPaymentMethod) -> {
val blikConfig: BlikConfiguration =
getConfigurationForPaymentMethod(storedPaymentMethod, dropInConfiguration)
@@ -450,6 +534,7 @@ internal fun getComponentFor(
key = storedPaymentMethod.id
)
}
+
else -> {
throw CheckoutException("Unable to find stored component for type - ${storedPaymentMethod.type}")
}
@@ -474,10 +559,11 @@ internal fun getComponentFor(
): PaymentComponent {
val dropInParams = dropInConfiguration.mapToParams(amount)
val sessionParams = sessionDetails?.mapToParams(amount)
+ val context = fragment.requireContext()
return when {
ACHDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val configuration: ACHDirectDebitConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
ACHDirectDebitComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -485,9 +571,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val bacsConfiguration: BacsDirectDebitConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
BacsDirectDebitComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -495,9 +582,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
BcmcComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val bcmcConfiguration: BcmcConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
BcmcComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -505,9 +593,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
BlikComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val blikConfiguration: BlikConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
BlikComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -515,9 +604,21 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
+ BoletoComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
+ val boletoConfiguration: BoletoConfiguration =
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
+ BoletoComponentProvider(dropInParams, sessionParams).get(
+ fragment = fragment,
+ paymentMethod = paymentMethod,
+ configuration = boletoConfiguration,
+ callback = componentCallback as ComponentCallback,
+ )
+ }
+
CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val cardConfig: CardConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
CardComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -525,9 +626,21 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
+ 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,
+ )
+ }
+
ConvenienceStoresJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val convenienceStoresJPConfiguration: ConvenienceStoresJPConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
ConvenienceStoresJPComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -535,9 +648,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
DotpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val dotpayConfig: DotpayConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
DotpayComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -545,9 +659,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
EntercashComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val entercashConfig: EntercashConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
EntercashComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -555,9 +670,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
EPSComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val epsConfig: EPSConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
EPSComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -565,9 +681,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val giftcardConfiguration: GiftCardConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
GiftCardComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -575,9 +692,10 @@ internal fun getComponentFor(
callback = componentCallback as GiftCardComponentCallback,
)
}
+
GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val googlePayConfiguration: GooglePayConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
GooglePayComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -585,9 +703,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
IdealComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val idealConfig: IdealConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
IdealComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -595,9 +714,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
InstantPaymentComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val instantPaymentConfiguration: InstantPaymentConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
InstantPaymentComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -605,9 +725,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
MBWayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val mbWayConfiguration: MBWayConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
MBWayComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -615,9 +736,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
MolpayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val molpayConfig: MolpayConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
MolpayComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -625,9 +747,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
OnlineBankingCZComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val onlineBankingCZConfig: OnlineBankingCZConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
OnlineBankingCZComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -635,9 +758,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
OnlineBankingJPComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val onlineBankingJPConfig: OnlineBankingJPConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
OnlineBankingJPComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -645,9 +769,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
OnlineBankingPLComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val onlineBankingPLConfig: OnlineBankingPLConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
OnlineBankingPLComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -655,9 +780,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
OnlineBankingSKComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val onlineBankingSKConfig: OnlineBankingSKConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
OnlineBankingSKComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -665,9 +791,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
OpenBankingComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val openBankingConfig: OpenBankingConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
OpenBankingComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -675,9 +802,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
PayByBankComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val payByBankConfig: PayByBankConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
PayByBankComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -685,9 +813,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
PayEasyComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val payEasyConfiguration: PayEasyConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
PayEasyComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -695,9 +824,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
SepaComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val sepaConfiguration: SepaConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
SepaComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -705,9 +835,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
SevenElevenComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val sevenElevenConfiguration: SevenElevenConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
SevenElevenComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -715,9 +846,10 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
UPIComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) -> {
val upiConfiguration: UPIConfiguration =
- getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration)
+ getConfigurationForPaymentMethod(paymentMethod, dropInConfiguration, context)
UPIComponentProvider(dropInParams, sessionParams).get(
fragment = fragment,
paymentMethod = paymentMethod,
@@ -725,6 +857,7 @@ internal fun getComponentFor(
callback = componentCallback as ComponentCallback,
)
}
+
else -> {
throw CheckoutException("Unable to find component for type - ${paymentMethod.type}")
}
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 34980dcb34..0c68241fa9 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
@@ -38,8 +38,9 @@ import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
@Suppress("TooManyFunctions")
+abstract class BaseDropInService
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-abstract class BaseDropInService : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropInServiceContract {
+constructor() : Service(), CoroutineScope, BaseDropInServiceInterface, BaseDropInServiceContract {
private val coroutineJob: Job = Job()
final override val coroutineContext: CoroutineContext get() = Dispatchers.Main + coroutineJob
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 4b75027714..f2dd94131d 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
@@ -20,9 +20,9 @@ import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
-import com.adyen.checkout.action.GenericActionComponent
-import com.adyen.checkout.action.GenericActionConfiguration
-import com.adyen.checkout.action.internal.provider.GenericActionComponentProvider
+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.ComponentError
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 652fa52831..81fafccda4 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
@@ -142,7 +142,7 @@ internal abstract class BaseComponentDialogFragment :
}
override fun onAdditionalDetails(actionComponentData: ActionComponentData) {
- throw IllegalStateException("This event should not be used in drop-in")
+ error("This event should not be used in drop-in")
}
override fun onError(componentError: ComponentError) {
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 7dfd302148..9f0bdd0838 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
@@ -35,6 +35,7 @@ import com.adyen.checkout.components.core.StoredPaymentMethod
import com.adyen.checkout.components.core.action.Action
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.dropin.BalanceDropInServiceResult
import com.adyen.checkout.dropin.BaseDropInServiceResult
import com.adyen.checkout.dropin.DropIn
@@ -182,7 +183,7 @@ internal class DropInActivity :
}
private fun createLocalizedContext(baseContext: Context?): Context? {
- if (baseContext == null) return baseContext
+ 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.
@@ -190,6 +191,8 @@ internal class DropInActivity :
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)
@@ -338,12 +341,16 @@ internal class DropInActivity :
val dialogFragment = when {
CardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
CardComponentDialogFragment.newInstance(paymentMethod)
+
BacsDirectDebitComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
BacsDirectDebitDialogFragment.newInstance(paymentMethod)
+
GiftCardComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
GiftCardComponentDialogFragment.newInstance(paymentMethod)
+
GooglePayComponent.PROVIDER.isPaymentMethodSupported(paymentMethod) ->
GooglePayComponentDialogFragment.newInstance(paymentMethod)
+
else -> GenericComponentDialogFragment.newInstance(paymentMethod)
}
@@ -449,6 +456,7 @@ internal class DropInActivity :
when (dropInServiceResult) {
is RecurringDropInServiceResult.PaymentMethodRemoved ->
handleRemovePaymentMethodResult(dropInServiceResult.id)
+
is RecurringDropInServiceResult.Error -> handleErrorDropInServiceResult(dropInServiceResult)
}
}
@@ -457,8 +465,10 @@ internal class DropInActivity :
when (dropInServiceResult) {
is SessionDropInServiceResult.SessionDataChanged ->
dropInViewModel.onSessionDataChanged(dropInServiceResult.sessionData)
+
is SessionDropInServiceResult.SessionTakenOverUpdated ->
dropInViewModel.onSessionTakenOverUpdated(dropInServiceResult.isFlowTakenOver)
+
is SessionDropInServiceResult.Error -> handleErrorDropInServiceResult(dropInServiceResult)
is SessionDropInServiceResult.Finished -> sendResult(dropInServiceResult.result)
}
@@ -522,7 +532,7 @@ internal class DropInActivity :
Logger.d(TAG, "handleIntent: action - ${intent.action}")
dropInViewModel.isWaitingResult = false
- if (WeChatPayUtils.isResultIntent(intent)) {
+ if (isWeChatPayIntent(intent)) {
Logger.d(TAG, "isResultIntent")
handleActionIntentResponse(intent)
}
@@ -537,12 +547,16 @@ internal class DropInActivity :
Logger.e(TAG, "Unexpected response from ACTION_VIEW - ${intent.data}")
}
}
+
else -> {
Logger.e(TAG, "Unable to find action")
}
}
}
+ private fun isWeChatPayIntent(intent: Intent): Boolean =
+ runCompileOnly { WeChatPayUtils.isResultIntent(intent) } ?: false
+
private fun handleActionIntentResponse(intent: Intent) {
val actionFragment = getActionFragment() ?: return
actionFragment.handleIntent(intent)
@@ -569,6 +583,7 @@ internal class DropInActivity :
setLoading(false)
showPaymentMethodsDialog()
}
+
is DropInActivityEvent.CancelOrder -> requestCancelOrderCall(event.order, event.isDropInCancelledByUser)
is DropInActivityEvent.CancelDropIn -> terminateWithError(DropIn.ERROR_REASON_USER_CANCELED)
is DropInActivityEvent.NavigateTo -> loadFragment(event.destination)
@@ -624,6 +639,7 @@ internal class DropInActivity :
result.reason,
result.terminateDropIn
)
+
is GiftCardBalanceResult.FullPayment -> handleGiftCardFullPayment(result)
is GiftCardBalanceResult.RequestOrderCreation -> requestOrdersCall()
is GiftCardBalanceResult.RequestPartialPayment -> requestPartialPayment()
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 98cc3f63f4..b6ea925be7 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
@@ -41,11 +41,8 @@ internal abstract class DropInBottomSheetDialogFragment : BottomSheetDialogFragm
override fun onAttach(context: Context) {
super.onAttach(context)
- if (activity is Protocol) {
- protocol = activity as Protocol
- } else {
- throw IllegalArgumentException("Host activity needs to implement DropInBottomSheetDialogFragment.Protocol")
- }
+ require(activity is Protocol)
+ protocol = activity as Protocol
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
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 625ae2eb62..652b4ea98a 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
@@ -92,7 +92,7 @@ internal class GooglePayComponentDialogFragment :
}
override fun onAdditionalDetails(actionComponentData: ActionComponentData) {
- throw IllegalStateException("This event should not be used in drop-in")
+ error("This event should not be used in drop-in")
}
override fun onError(componentError: ComponentError) {
diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodAdapter.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodAdapter.kt
index ea871ea6e9..6b07f6df62 100644
--- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodAdapter.kt
+++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/PaymentMethodAdapter.kt
@@ -155,7 +155,8 @@ internal class PaymentMethodAdapter @JvmOverloads constructor(
private fun bindGenericStored(model: GenericStoredModel) {
with(binding) {
textViewTitle.text = model.name
- textViewDetail.isVisible = false
+ textViewDetail.isVisible = !model.description.isNullOrEmpty()
+ textViewDetail.text = model.description
imageViewLogo.loadLogo(
environment = model.environment,
txVariant = model.imageId,
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 98b33d5512..c5c25741e4 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
@@ -76,7 +76,7 @@ internal class PaymentMethodsListViewModel(
private fun setupPaymentMethods(paymentMethods: List) {
paymentMethods.forEach { paymentMethod ->
- val type = paymentMethod.type ?: throw IllegalStateException("PaymentMethod type is null")
+ val type = requireNotNull(paymentMethod.type) { "PaymentMethod type is null" }
when {
PaymentMethodTypes.SUPPORTED_PAYMENT_METHODS.contains(type) -> {
@@ -201,8 +201,9 @@ internal class PaymentMethodsListViewModel(
private fun List.mapToPaymentMethodModelList(): List =
mapIndexedNotNull { index, paymentMethod ->
- val isAvailable = paymentMethodsAvailabilityMap[paymentMethod]
- ?: throw IllegalStateException("payment method not found in map")
+ val isAvailable = requireNotNull(paymentMethodsAvailabilityMap[paymentMethod]) {
+ "payment method not found in map"
+ }
if (isAvailable) paymentMethod.mapToModel(index) else null
}
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 ddd6bc017e..3d9fc60b97 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
@@ -130,6 +130,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF
DateUtils.parseDateToView(storedPaymentMethodModel.expiryMonth, storedPaymentMethodModel.expiryYear)
binding.storedPaymentMethodItem.textViewDetail.isVisible = true
}
+
is StoredACHDirectDebitModel -> {
binding.storedPaymentMethodItem.textViewTitle.text =
requireActivity().getString(
@@ -142,9 +143,12 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF
)
binding.storedPaymentMethodItem.textViewDetail.isVisible = false
}
+
is GenericStoredModel -> {
binding.storedPaymentMethodItem.textViewTitle.text = storedPaymentMethodModel.name
- binding.storedPaymentMethodItem.textViewDetail.isVisible = false
+ binding.storedPaymentMethodItem.textViewDetail.isVisible =
+ !storedPaymentMethodModel.description.isNullOrEmpty()
+ binding.storedPaymentMethodItem.textViewDetail.text = storedPaymentMethodModel.description
binding.storedPaymentMethodItem.imageViewLogo.loadLogo(
environment = dropInViewModel.dropInConfiguration.environment,
txVariant = storedPaymentMethodModel.imageId,
@@ -159,6 +163,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF
is ButtonState.ContinueButton -> {
binding.payButton.setText(buttonState.labelResId)
}
+
is ButtonState.PayButton -> {
binding.payButton.text = PayButtonFormatter.getPayButtonText(
amount = buttonState.amount,
@@ -166,6 +171,7 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF
localizedContext = requireContext(),
)
}
+
is ButtonState.Loading -> {
// already handled
}
@@ -183,9 +189,11 @@ internal class PreselectedStoredPaymentMethodFragment : DropInBottomSheetDialogF
is PreselectedStoredEvent.ShowStoredPaymentScreen -> {
protocol.showStoredComponentDialog(storedPaymentMethod, true)
}
+
is PreselectedStoredEvent.RequestPaymentsCall -> {
protocol.requestPaymentsCall(event.state)
}
+
is PreselectedStoredEvent.ShowError -> {
handleError(event.componentError)
}
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 9257150258..bb8e26ff39 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
@@ -57,7 +57,7 @@ internal class PreselectedStoredPaymentViewModel(
}
override fun onAdditionalDetails(actionComponentData: ActionComponentData) {
- throw IllegalStateException("This event should not be used in drop-in")
+ error("This event should not be used in drop-in")
}
override fun onError(componentError: ComponentError) {
diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/StoredPaymentMethodModel.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/StoredPaymentMethodModel.kt
index e2c136edcd..6a7945f015 100644
--- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/StoredPaymentMethodModel.kt
+++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/ui/model/StoredPaymentMethodModel.kt
@@ -43,6 +43,7 @@ internal data class GenericStoredModel(
override val imageId: String,
override val isRemovable: Boolean,
val name: String,
+ val description: String?,
// We need the environment to load the logo
val environment: Environment,
) : StoredPaymentMethodModel()
diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/StoredUtils.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/StoredUtils.kt
index 36b4b5b088..8940fe41d7 100644
--- a/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/StoredUtils.kt
+++ b/drop-in/src/main/java/com/adyen/checkout/dropin/internal/util/StoredUtils.kt
@@ -21,7 +21,7 @@ internal fun StoredPaymentMethod.mapStoredModel(
environment: Environment
): StoredPaymentMethodModel {
return when (this.type) {
- PaymentMethodTypes.SCHEME -> with(this) {
+ PaymentMethodTypes.SCHEME -> {
StoredCardModel(
id = id.orEmpty(),
imageId = brand.orEmpty(),
@@ -32,7 +32,8 @@ internal fun StoredPaymentMethod.mapStoredModel(
environment = environment,
)
}
- PaymentMethodTypes.ACH -> with(this) {
+
+ PaymentMethodTypes.ACH -> {
StoredACHDirectDebitModel(
id = id.orEmpty(),
imageId = type.orEmpty(),
@@ -41,11 +42,24 @@ internal fun StoredPaymentMethod.mapStoredModel(
environment = environment,
)
}
+
+ PaymentMethodTypes.CASH_APP_PAY -> {
+ GenericStoredModel(
+ id = id.orEmpty(),
+ imageId = type.orEmpty(),
+ isRemovable = isRemovingEnabled,
+ name = cashtag.orEmpty(),
+ description = name,
+ environment = environment,
+ )
+ }
+
else -> GenericStoredModel(
id = id.orEmpty(),
imageId = type.orEmpty(),
isRemovable = isRemovingEnabled,
name = name.orEmpty(),
+ description = null,
environment = environment,
)
}
diff --git a/drop-in/src/main/res/layout/fragment_generic_action_component.xml b/drop-in/src/main/res/layout/fragment_generic_action_component.xml
index 0fce6d0112..27c3b57410 100644
--- a/drop-in/src/main/res/layout/fragment_generic_action_component.xml
+++ b/drop-in/src/main/res/layout/fragment_generic_action_component.xml
@@ -40,7 +40,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
-