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" /> -