diff --git a/.github/workflows/check_develop.yml b/.github/workflows/check_develop.yml new file mode 100644 index 0000000000..d312ffd653 --- /dev/null +++ b/.github/workflows/check_develop.yml @@ -0,0 +1,16 @@ +name: Check Develop + +on: + push: + branches: + - 'develop' + +concurrency: + group: 'develop' + cancel-in-progress: true + +jobs: + sonar_cloud: + name: SonarCloud + uses: ./.github/workflows/sonar_cloud.yml + secrets: inherit diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml index 298cc0bedd..8e73cb4e6c 100644 --- a/.github/workflows/check_pr.yml +++ b/.github/workflows/check_pr.yml @@ -22,3 +22,7 @@ jobs: name: Test uses: ./.github/workflows/run_tests.yml needs: assemble + sonar_cloud: + name: SonarCloud + uses: ./.github/workflows/sonar_cloud.yml + secrets: inherit diff --git a/.github/workflows/check_release_notes.yml b/.github/workflows/check_release_notes.yml new file mode 100644 index 0000000000..baf9ddd762 --- /dev/null +++ b/.github/workflows/check_release_notes.yml @@ -0,0 +1,35 @@ +name: Check Release Notes + +# Every PR with a label should include an update to the release notes +on: + pull_request: + branches-ignore: + - 'main' + types: [ opened, synchronize, reopened, labeled, unlabeled ] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + release-notes-check: + # https://github.com/actions/virtual-environments/ + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Check release notes + if: | + contains(github.event.pull_request.labels.*.name, 'Breaking change') || + contains(github.event.pull_request.labels.*.name, 'Feature') || + contains(github.event.pull_request.labels.*.name, 'Fix') + run: | + git fetch origin develop --depth 1 + if [ -n "$(git diff origin/develop RELEASE_NOTES.md)" ] + then + echo "RELEASE_NOTES.md was updated" + exit 0 + else + echo "::error::Add release notes for your PR by updating RELEASE_NOTES.md" + exit 1 + fi diff --git a/.github/workflows/publish_docs.yml b/.github/workflows/publish_docs.yml index ada8db7a53..a9785155b0 100644 --- a/.github/workflows/publish_docs.yml +++ b/.github/workflows/publish_docs.yml @@ -34,7 +34,7 @@ jobs: # Deploy to GitHub Pages - name: Deploy GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4.4.3 + uses: JamesIves/github-pages-deploy-action@v4.5.0 with: BRANCH: gh-pages FOLDER: build/docs/ diff --git a/.github/workflows/sonar_cloud.yml b/.github/workflows/sonar_cloud.yml new file mode 100644 index 0000000000..e6f9d7acd6 --- /dev/null +++ b/.github/workflows/sonar_cloud.yml @@ -0,0 +1,34 @@ +name: SonarCloud + +on: + workflow_call + +jobs: + sonar_cloud: + name: Run SonarCloud + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '0' + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 17 + cache: 'gradle' + + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew detekt assDeb teDebUnTe jacocoDebugTestReport lintDeb sonar diff --git a/README.md b/README.md index 737d927f57..647b5d1fb5 100644 --- a/README.md +++ b/README.md @@ -31,27 +31,31 @@ Import the corresponding module in your `build.gradle` file. For Drop-in: ```groovy -implementation "com.adyen.checkout:drop-in-compose:5.1.0" +implementation "com.adyen.checkout:drop-in-compose:5.2.0" ``` For the Credit Card component: ```groovy -implementation "com.adyen.checkout:card:5.1.0" -implementation "com.adyen.checkout:components-compose:5.1.0" +implementation "com.adyen.checkout:card:5.2.0" +implementation "com.adyen.checkout:components-compose:5.2.0" ``` ### Without Jetpack Compose For Drop-in: ```groovy -implementation "com.adyen.checkout:drop-in:5.1.0" +implementation "com.adyen.checkout:drop-in:5.2.0" ``` For the Credit Card component: ```groovy -implementation "com.adyen.checkout:card:5.1.0" +implementation "com.adyen.checkout:card:5.2.0" ``` The library is available on [Maven Central][mavenRepo]. +## UI Customization + +[See the UI Customization Guide for more.](docs/UI_CUSTOMIZATION.md) + ## Migrate from v4 If you are upgrading from 4.x.x to a current release, check out our [migration guide][migration.guide]. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 102bce94d9..a5300f72d8 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,5 @@ [//]: # (This file will be used for the release notes on GitHub when publishing.) -[//]: # (Types of changes: `Breaking changes` `New` `Added` `Changed` `Deprecated` `Removed` `Fixed`) +[//]: # (Types of changes: `Breaking changes` `New` `Added` `Improved` `Changed` `Deprecated` `Removed` `Fixed`) [//]: # (Example:) [//]: # (## Added) [//]: # ( - New payment method) @@ -9,27 +9,22 @@ [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) ## New -- The [BcmcComponent](https://adyen.github.io/adyen-android/bcmc/com.adyen.checkout.bcmc/-bcmc-component/index.html) now supports co-badged Bancontact cards and card brand detection. - - The [BcmcComponentState](https://adyen.github.io/adyen-android/bcmc/com.adyen.checkout.bcmc/-bcmc-component-state/index.html) now contains 3 extra fields: `cardBrand`, `binValue` and `lastFourDigits`. -- You can now override payment method names in Drop-in by using [DropInConfiguration.Builder.overridePaymentMethodName(type, name)](https://adyen.github.io/adyen-android/drop-in/com.adyen.checkout.dropin/-drop-in-configuration/-builder/override-payment-method-name.html). -- For stored cards, Drop-in now shows the card name (for example **Visa** or **Mastercard**) instead of **Credit Card**. -- Now it is possible to show installment amounts for card payments using [InstallmentConfiguration.showInstallmentAmount](https://adyen.github.io/adyen-android/card/com.adyen.checkout.card/-installment-configuration/show-installment-amount.html) in [CardConfiguration.Builder.setInstallmentConfigurations()](https://adyen.github.io/adyen-android/card/com.adyen.checkout.card/-card-configuration/-builder/set-installment-configurations.html). -- For gift cards, you can now hide the PIN text field by setting [GiftCardConfiguration.Builder.setPinRequired()](https://adyen.github.io/adyen-android/giftcard/com.adyen.checkout.giftcard/-gift-card-configuration/-builder/set-pin-required.html) to **false**. -- For Google Pay: - - When initializing the [Google Pay button](https://docs.adyen.com/payment-methods/google-pay/android-component/#2-show-the-google-pay-button), you can now use [GooglePayComponent.getGooglePayButtonParameters()](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-google-pay-component/get-google-pay-button-parameters.html) to get the `allowedPaymentMethods` attribute. - - You can now use [AllowedAuthMethods](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-allowed-auth-methods/index.html) and [AllowedCardNetworks](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-allowed-card-networks/index.html) to easily access to the possible values for [GooglePayConfiguration.Builder.setAllowedAuthMethods()](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-google-pay-configuration/-builder/set-allowed-auth-methods.html) and [GooglePayConfiguration.Builder.setAllowedCardNetworks()](https://adyen.github.io/adyen-android/googlepay/com.adyen.checkout.googlepay/-google-pay-configuration/-builder/set-allowed-card-networks.html). +- We added a [UI customization guide](docs/UI_CUSTOMIZATION.md), which explains how to customize the styles and string resources. + +## Improved +- The integration now uses JSON Web Encryption (JWE) with RSA OAEP 256 and AES GCM 256 for encryption. You do not need to make any changes to your integration. ## Fixed -- Fixed a bug where components would not be displayed in Jetpack Compose lazy lists. +- For Drop-in, error dialogs no longer display user unfriendly messages when using the Sessions flow. +- Overriding some of the XML styles without specifying a parent style no longer causes a build error. +- The Await and QR Code action components no longer get stuck in a loading state after the payment is completed. ## Changed - Dependency versions: | Name | Version | |--------------------------------------------------------------------------------------------------------|-------------------------------| - | [AndroidX Compose Activity](https://developer.android.com/jetpack/androidx/releases/activity#1.8.0) | **1.8.0** | - | [Material Design](https://m2.material.io/) | **1.10.0** | - | [Gradle](https://docs.gradle.org/8.4/release-notes.html) | **8.4** | - | [Android Gradle plugin](https://developer.android.com/build/releases/gradle-plugin) | **8.1.2** | - | [AndroidX Compose BoM](https://developer.android.com/jetpack/compose/bom/bom-mapping) | **2023.10.01** | - | [AndroidX Recyclerview](https://developer.android.com/jetpack/androidx/releases/recyclerview#1.3.2) | **1.3.2** | - | [AndroidX Fragment](https://developer.android.com/jetpack/androidx/releases/fragment#1.6.2) | **1.6.2** | + | [Kotlin](https://kotlinlang.org/docs/releases.html#release-details) | **1.9.21** | + | [Android Gradle plugin](https://developer.android.com/build/releases/gradle-plugin) | **8.2.0** | + | [AndroidX Compose compiler](https://developer.android.com/jetpack/androidx/releases/compose-compiler) | **1.5.7** | + | [AndroidX Compose Activity](https://developer.android.com/jetpack/androidx/releases/activity#1.8.1) | **1.8.1** | + | [AndroidX Browser](https://developer.android.com/jetpack/androidx/releases/browser#1.7.0) | **1.7.0** | diff --git a/SECURITY.md b/SECURITY.md index 8a471c25d0..e843ff246f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,4 +1,3 @@ -# Reporting Security Issues +# Disclosing security issues -We welcome reports of possible vulnerabilities or issues as part of our responsible disclosure program. For more information go to -https://support.adyen.com/hc/en-us/articles/115001187330-How-do-I-report-a-possible-security-issue-to-Adyen +We welcome reports of possible vulnerabilities or issues as part of our [responsible disclosure policy](https://www.adyen.com/policies-and-disclaimer/responsible-disclosure). For more information check out this page on [how to disclose a security issue](https://help.adyen.com/en_US/knowledge/security/product-security/how-do-i-disclose-a-security-issue). 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 10593a7678..a828293980 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 @@ -43,9 +43,7 @@ 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.cse.internal.ClientSideEncrypter -import com.adyen.checkout.cse.internal.DateGenerator -import com.adyen.checkout.cse.internal.DefaultGenericEncrypter +import com.adyen.checkout.cse.internal.GenericEncryptorFactory import com.adyen.checkout.sessions.core.CheckoutSession import com.adyen.checkout.sessions.core.SessionComponentCallback import com.adyen.checkout.sessions.core.internal.SessionComponentEventHandler @@ -117,9 +115,7 @@ constructor( val addressService = AddressService(httpClient) val addressRepository = DefaultAddressRepository(addressService) - val dateGenerator = DateGenerator() - val clientSideEncrypter = ClientSideEncrypter() - val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) + val genericEncryptor = GenericEncryptorFactory.provide() val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( application = application, @@ -139,7 +135,7 @@ constructor( publicKeyRepository = publicKeyRepository, addressRepository = addressRepository, submitHandler = SubmitHandler(savedStateHandle), - genericEncrypter = genericEncrypter, + genericEncryptor = genericEncryptor, componentParams = componentParams, order = order ) @@ -191,9 +187,7 @@ constructor( val addressService = AddressService(httpClient) val addressRepository = DefaultAddressRepository(addressService) - val dateGenerator = DateGenerator() - val clientSideEncrypter = ClientSideEncrypter() - val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) + val genericEncryptor = GenericEncryptorFactory.provide() val analyticsRepository = analyticsRepository ?: DefaultAnalyticsRepository( analyticsRepositoryData = AnalyticsRepositoryData( application = application, @@ -214,7 +208,7 @@ constructor( publicKeyRepository = publicKeyRepository, addressRepository = addressRepository, submitHandler = SubmitHandler(savedStateHandle), - genericEncrypter = genericEncrypter, + genericEncryptor = genericEncryptor, componentParams = componentParams, order = checkoutSession.order ) diff --git a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt index c300fcc50d..f7b0995eb9 100644 --- a/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt +++ b/ach/src/main/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegate.kt @@ -29,7 +29,7 @@ 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.cse.EncryptionException -import com.adyen.checkout.cse.internal.BaseGenericEncrypter +import com.adyen.checkout.cse.internal.BaseGenericEncryptor import com.adyen.checkout.ui.core.internal.data.api.AddressRepository import com.adyen.checkout.ui.core.internal.ui.AddressFormUIState import com.adyen.checkout.ui.core.internal.ui.ButtonComponentViewType @@ -66,7 +66,7 @@ internal class DefaultACHDirectDebitDelegate( private val publicKeyRepository: PublicKeyRepository, private val addressRepository: AddressRepository, private val submitHandler: SubmitHandler, - private val genericEncrypter: BaseGenericEncrypter, + private val genericEncryptor: BaseGenericEncryptor, override val componentParams: ACHDirectDebitComponentParams, private val order: Order? ) : ACHDirectDebitDelegate, ButtonDelegate, UIStateDelegate { @@ -277,12 +277,12 @@ internal class DefaultACHDirectDebitDelegate( } try { - val encryptedBankAccountNumber = genericEncrypter.encryptField( + val encryptedBankAccountNumber = genericEncryptor.encryptField( fieldKeyToEncrypt = ENCRYPTION_KEY_FOR_BANK_ACCOUNT_NUMBER, fieldValueToEncrypt = outputData.bankAccountNumber.value, publicKey = publicKey ) - val encryptedBankLocationId = genericEncrypter.encryptField( + val encryptedBankLocationId = genericEncryptor.encryptField( fieldKeyToEncrypt = ENCRYPTION_KEY_FOR_BANK_LOCATION_ID, fieldValueToEncrypt = outputData.bankLocationId.value, publicKey = publicKey diff --git a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt index abd8899460..172965f178 100644 --- a/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt +++ b/ach/src/test/java/com/adyen/checkout/ach/internal/ui/DefaultACHDirectDebitDelegateTest.kt @@ -26,8 +26,8 @@ import com.adyen.checkout.components.core.internal.ui.model.FieldState import com.adyen.checkout.components.core.internal.ui.model.Validation import com.adyen.checkout.components.core.paymentmethod.ACHDirectDebitPaymentMethod import com.adyen.checkout.core.Environment -import com.adyen.checkout.cse.internal.BaseGenericEncrypter -import com.adyen.checkout.cse.internal.test.TestGenericEncrypter +import com.adyen.checkout.cse.internal.BaseGenericEncryptor +import com.adyen.checkout.cse.internal.test.TestGenericEncryptor import com.adyen.checkout.test.TestDispatcherExtension import com.adyen.checkout.test.extensions.test import com.adyen.checkout.ui.core.internal.data.api.AddressRepository @@ -72,14 +72,14 @@ internal class DefaultACHDirectDebitDelegateTest( private lateinit var publicKeyRepository: TestPublicKeyRepository private lateinit var addressRepository: TestAddressRepository - private lateinit var genericEncrypter: TestGenericEncrypter + private lateinit var genericEncryptor: TestGenericEncryptor private lateinit var delegate: DefaultACHDirectDebitDelegate @BeforeEach fun setUp() { publicKeyRepository = TestPublicKeyRepository() addressRepository = TestAddressRepository() - genericEncrypter = TestGenericEncrypter() + genericEncryptor = TestGenericEncryptor() delegate = createAchDelegate() } @@ -325,7 +325,7 @@ internal class DefaultACHDirectDebitDelegateTest( @Test fun `encryption fails, then component state should be invalid`() = runTest { - genericEncrypter.shouldThrowException = true + genericEncryptor.shouldThrowException = true delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -676,7 +676,7 @@ internal class DefaultACHDirectDebitDelegateTest( analyticsRepository: AnalyticsRepository = this.analyticsRepository, publicKeyRepository: PublicKeyRepository = this.publicKeyRepository, addressRepository: AddressRepository = this.addressRepository, - genericEncrypter: BaseGenericEncrypter = this.genericEncrypter, + genericEncryptor: BaseGenericEncryptor = this.genericEncryptor, submitHandler: SubmitHandler = this.submitHandler, configuration: ACHDirectDebitConfiguration = getAchConfigurationBuilder().build(), order: OrderRequest? = TEST_ORDER, @@ -687,7 +687,7 @@ internal class DefaultACHDirectDebitDelegateTest( publicKeyRepository = publicKeyRepository, addressRepository = addressRepository, submitHandler = submitHandler, - genericEncrypter = genericEncrypter, + genericEncryptor = genericEncryptor, componentParams = ACHDirectDebitComponentParamsMapper(null, null).mapToParams(configuration, null), order = order ) diff --git a/bacs/src/main/res/values/styles.xml b/bacs/src/main/res/values/styles.xml index 1fa82c035a..922ed67ef5 100644 --- a/bacs/src/main/res/values/styles.xml +++ b/bacs/src/main/res/values/styles.xml @@ -31,6 +31,8 @@ textEmailAddress + +``` + +### Option 3: Keep default Adyen theme + +You can keep the default Adyen theme by simply adding one line. Optionally, you can change some attributes. + +```XML + +``` + +To figure out all the colors and styling you can override have a look at [the Material guides](https://m2.material.io/design/color/the-color-system.html) or check [our Adyen base style](https://github.com/Adyen/adyen-android/blob/main/ui-core/src/main/res/values/styles.xml). + +## Customizing the style for specific view types + +In case you want to change the styling for a specific view type, you can do this by copying the base styling into your `styles.xml` and change the values as you like. For example, if you want to change the border color of every `TextInputLayout` to red: + +```XML + +``` + +It can be difficult to find which style is applied to which view. To figure this out we recommend to take a look at [our styles.xml](https://github.com/Adyen/adyen-android/blob/main/ui-core/src/main/res/values/styles.xml) or use the Layout Inspector. + +## Customizing a specific view + +Every view has its own style that builds on top of the view type style. This allows you to customize a specific view. Take for example the card number input field, copy the base style in your `styles.xml` + +```XML + +``` + +## Adding dark mode support +Out of the box the SDK doesn’t support dark mode, but you can easily add this with some additional setup. Copy the base `AdyenCheckout` style in your `styles.xml` and change the parent to a Material DayNight theme. Now you can use the `values-night` resource folder to define colors and styles to be used in dark mode. + +```XML + +``` + +To learn more about Material dark theming you can read [this guide](https://m2.material.io/develop/android/theming/dark) or take a look at [our example app](https://github.com/Adyen/adyen-android/blob/main/example-app/src/main/res/values/styles.xml). + +## Overriding string resources + +It is possible to change text in the SDK by overriding string resources. To override a string resource you have to copy the string resource in your own strings.xml. The easiest way to find the string resource is to search for the string on Github. In case your app supports multiple languages you can do the exact same thing, but make sure to use the right `values` directory (for example `values-nl-rNL`). + +```XML + + + More Payment Methods + +``` + +Payment method names in the payment method list can be overridden with a configuration object: + +```kotlin +DropInConfiguration.Builder(shopperLocale, environment, clientKey) + .overridePaymentMethodName(PaymentMethodTypes.SCHEME, "Credit cards") + .overridePaymentMethodName(PaymentMethodTypes.GIFTCARD, "Specific gift card") + .build() +``` + +If you cannot find a certain string in the code base, then check whether it is coming from the Checkout API. Make sure you localize these strings by sending the correct [shopperLocale](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions#request-shopperLocale). diff --git a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt index 414f9b7277..d6ea5b381f 100644 --- a/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt +++ b/drop-in/src/main/java/com/adyen/checkout/dropin/SessionDropInService.kt @@ -88,15 +88,17 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter is SessionCallResult.Payments.Action -> DropInServiceResult.Action(result.action) is SessionCallResult.Payments.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(message = result.throwable.message), - dismissDropIn = true + errorDialog = ErrorDialog(), + reason = result.throwable.message, + dismissDropIn = true, ) is SessionCallResult.Payments.Finished -> SessionDropInServiceResult.Finished(result.result) is SessionCallResult.Payments.NotFullyPaidOrder -> updatePaymentMethods(result.result.order) is SessionCallResult.Payments.RefusedPartialPayment -> DropInServiceResult.Error( - errorDialog = ErrorDialog(message = "Payment is refused while making a partial payment.") + errorDialog = ErrorDialog(), + reason = "Payment was refused while making a partial payment", ) is SessionCallResult.Payments.TakenOver -> { @@ -114,15 +116,16 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter val result = sessionInteractor.onDetailsCallRequested( actionComponentData, ::onAdditionalDetails, - ::onAdditionalDetails.name + ::onAdditionalDetails.name, ) val dropInServiceResult = when (result) { is SessionCallResult.Details.Action -> DropInServiceResult.Action(result.action) is SessionCallResult.Details.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(message = result.throwable.message), - dismissDropIn = true + errorDialog = ErrorDialog(), + reason = result.throwable.message, + dismissDropIn = true, ) is SessionCallResult.Details.Finished -> SessionDropInServiceResult.Finished(result.result) @@ -146,7 +149,10 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter val dropInServiceResult = when (result) { is SessionCallResult.Balance.Error -> - BalanceDropInServiceResult.Error(errorDialog = ErrorDialog(message = result.throwable.message)) + BalanceDropInServiceResult.Error( + errorDialog = ErrorDialog(), + reason = result.throwable.message, + ) is SessionCallResult.Balance.Successful -> BalanceDropInServiceResult.Balance(result.balanceResult) SessionCallResult.Balance.TakenOver -> { @@ -163,14 +169,15 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter launch { val result = sessionInteractor.createOrder( ::onOrderRequest, - ::onOrderRequest.name + ::onOrderRequest.name, ) val dropInServiceResult = when (result) { is SessionCallResult.CreateOrder.Error -> OrderDropInServiceResult.Error( - errorDialog = ErrorDialog(message = result.throwable.message), - dismissDropIn = true + errorDialog = ErrorDialog(), + reason = result.throwable.message, + dismissDropIn = true, ) is SessionCallResult.CreateOrder.Successful -> OrderDropInServiceResult.OrderCreated(result.order) @@ -190,12 +197,15 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter val result = sessionInteractor.cancelOrder( order, { onOrderCancel(it, shouldUpdatePaymentMethods) }, - ::onOrderCancel.name + ::onOrderCancel.name, ) val dropInServiceResult = when (result) { is SessionCallResult.CancelOrder.Error -> - DropInServiceResult.Error(errorDialog = ErrorDialog(message = result.throwable.message)) + DropInServiceResult.Error( + errorDialog = ErrorDialog(), + reason = result.throwable.message, + ) SessionCallResult.CancelOrder.Successful -> { if (!shouldUpdatePaymentMethods) return@launch @@ -216,13 +226,14 @@ open class SessionDropInService : BaseDropInService(), SessionDropInServiceInter return when (val result = sessionInteractor.updatePaymentMethods(order)) { is SessionCallResult.UpdatePaymentMethods.Successful -> DropInServiceResult.Update( result.paymentMethods, - result.order + result.order, ) is SessionCallResult.UpdatePaymentMethods.Error -> DropInServiceResult.Error( - errorDialog = ErrorDialog(message = result.throwable.message), - dismissDropIn = true + errorDialog = ErrorDialog(), + reason = result.throwable.message, + dismissDropIn = true, ) } } diff --git a/example-app/build.gradle b/example-app/build.gradle index 61079a8e5e..6a523c3a96 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -68,7 +68,7 @@ dependencies { // Checkout implementation project(':drop-in') implementation project(':components-compose') -// implementation "com.adyen.checkout:drop-in:5.1.0" +// implementation "com.adyen.checkout:drop-in:5.2.0" // Dependencies implementation libraries.kotlinCoroutines @@ -94,6 +94,8 @@ dependencies { implementation libraries.hilt kapt libraries.hiltCompiler + implementation libraries.googlePay.composeButton + debugImplementation libraries.leakCanary debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' diff --git a/example-app/src/main/AndroidManifest.xml b/example-app/src/main/AndroidManifest.xml index 0ad79d28d0..03f4ca2547 100644 --- a/example-app/src/main/AndroidManifest.xml +++ b/example-app/src/main/AndroidManifest.xml @@ -187,6 +187,32 @@ android:value=".ui.main.MainActivity" /> + + + + + + + + + + + + + + + diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt index 9da64a0fea..ca01899b46 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/bacs/BacsFragment.kt @@ -22,8 +22,8 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.adyen.checkout.bacs.BacsDirectDebitComponent import com.adyen.checkout.example.databinding.FragmentBacsBinding +import com.adyen.checkout.example.extensions.getLogTag import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider -import com.adyen.checkout.example.ui.instant.InstantFragment import com.adyen.checkout.redirect.RedirectComponent import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -49,7 +49,7 @@ class BacsFragment : BottomSheetDialogFragment() { // Insert return url in extras, so we can access it in the ViewModel through SavedStateHandle val returnUrl = RedirectComponent.getReturnUrl(requireActivity().applicationContext) arguments = (arguments ?: bundleOf()).apply { - putString(InstantFragment.RETURN_URL_EXTRA, returnUrl) + putString(RETURN_URL_EXTRA, returnUrl) } _binding = FragmentBacsBinding.inflate(inflater) @@ -144,7 +144,7 @@ class BacsFragment : BottomSheetDialogFragment() { companion object { - private const val TAG = "BacsFragment" + private val TAG = getLogTag() internal const val RETURN_URL_EXTRA = "RETURN_URL_EXTRA" diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt index 64c7bcf984..4bf47716b4 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/card/compose/SessionsCardActivity.kt @@ -12,10 +12,8 @@ import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.isSystemInDarkTheme import androidx.core.view.WindowCompat import com.adyen.checkout.example.ui.theme.ExampleTheme -import com.adyen.checkout.example.ui.theme.NightTheme import com.adyen.checkout.example.ui.theme.NightThemeRepository import com.adyen.checkout.redirect.RedirectComponent import dagger.hilt.android.AndroidEntryPoint @@ -38,12 +36,8 @@ class SessionsCardActivity : AppCompatActivity() { intent = (intent ?: Intent()).putExtra(RETURN_URL_EXTRA, returnUrl) setContent { - val useDarkTheme = when (nightThemeRepository.theme) { - NightTheme.DAY -> false - NightTheme.NIGHT -> true - NightTheme.SYSTEM -> isSystemInDarkTheme() - } - ExampleTheme(useDarkTheme) { + val isDarkTheme = nightThemeRepository.isDarkTheme() + ExampleTheme(isDarkTheme) { SessionsCardScreen(onBackPressed = { onBackPressedDispatcher.onBackPressed() }) } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt index 50202b5b94..db8117843b 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/CheckoutConfigurationProvider.kt @@ -129,7 +129,7 @@ internal class CheckoutConfigurationProvider @Inject constructor( .setAnalyticsConfiguration(getAnalyticsConfiguration()) .build() - private fun getGooglePayConfiguration(): GooglePayConfiguration = + fun getGooglePayConfiguration(): GooglePayConfiguration = GooglePayConfiguration.Builder(shopperLocale, environment, clientKey) .setCountryCode(keyValueStorage.getCountry()) .setAmount(amount) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayActivityResult.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayActivityResult.kt new file mode 100644 index 0000000000..019178c53e --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayActivityResult.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 13/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay + +import android.content.Intent +import com.adyen.checkout.example.ui.googlepay.compose.SessionsGooglePayComponentData + +internal data class GooglePayActivityResult( + val componentData: SessionsGooglePayComponentData, + val resultCode: Int, + val data: Intent?, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt new file mode 100644 index 0000000000..3f28bb0f43 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayComponentData.kt @@ -0,0 +1,20 @@ +/* + * 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/8/2023. + */ + +package com.adyen.checkout.example.ui.googlepay + +import com.adyen.checkout.components.core.ComponentCallback +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.googlepay.GooglePayComponentState +import com.adyen.checkout.googlepay.GooglePayConfiguration + +internal data class GooglePayComponentData( + val paymentMethod: PaymentMethod, + val googlePayConfiguration: GooglePayConfiguration, + val callback: ComponentCallback, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayEvent.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayEvent.kt new file mode 100644 index 0000000000..1216561e2d --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayEvent.kt @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2023 Adyen N.V. + * + * This file is open source and available under the MIT license. See the LICENSE file for more info. + * + * Created by josephj on 17/8/2023. + */ + +package com.adyen.checkout.example.ui.googlepay + +import com.adyen.checkout.components.core.action.Action + +internal sealed class GooglePayEvent { + + data class PaymentResult(val result: String) : GooglePayEvent() + + data class AdditionalAction(val action: Action) : GooglePayEvent() +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt new file mode 100644 index 0000000000..0184f0a707 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayFragment.kt @@ -0,0 +1,181 @@ +/* + * 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/8/2023. + */ + +package com.adyen.checkout.example.ui.googlepay + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.os.bundleOf +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.adyen.checkout.example.databinding.FragmentGooglePayBinding +import com.adyen.checkout.example.extensions.getLogTag +import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider +import com.adyen.checkout.googlepay.GooglePayComponent +import com.adyen.checkout.redirect.RedirectComponent +import com.google.android.gms.wallet.button.ButtonConstants.ButtonType +import com.google.android.gms.wallet.button.ButtonOptions +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@AndroidEntryPoint +class GooglePayFragment : BottomSheetDialogFragment() { + + @Inject + internal lateinit var checkoutConfigurationProvider: CheckoutConfigurationProvider + + private var _binding: FragmentGooglePayBinding? = null + private val binding: FragmentGooglePayBinding get() = requireNotNull(_binding) + + private val viewModel: GooglePayViewModel by viewModels() + + private var googlePayComponent: GooglePayComponent? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + // Insert return url in extras, so we can access it in the ViewModel through SavedStateHandle + val returnUrl = RedirectComponent.getReturnUrl(requireActivity().applicationContext) + arguments = (arguments ?: bundleOf()).apply { + putString(RETURN_URL_EXTRA, returnUrl) + } + + _binding = FragmentGooglePayBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.googleComponentDataFlow + .flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { setupGooglePayComponent(it) } + .launchIn(lifecycleScope) + + viewModel.viewState + .flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { onViewState(it) } + .launchIn(lifecycleScope) + + viewModel.events + .flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { onEvent(it) } + .launchIn(lifecycleScope) + } + + private fun setupGooglePayComponent(googlePayComponentData: GooglePayComponentData) { + val googlePayComponent = with(googlePayComponentData) { + GooglePayComponent.PROVIDER.get( + fragment = this@GooglePayFragment, + paymentMethod = paymentMethod, + configuration = googlePayConfiguration, + callback = callback, + ) + } + + this.googlePayComponent = googlePayComponent + + binding.componentView.attach(googlePayComponent, viewLifecycleOwner) + + loadGooglePayButton() + } + + private fun onViewState(state: GooglePayViewState) { + when (state) { + is GooglePayViewState.Error -> { + binding.errorView.isVisible = true + binding.errorView.text = getString(state.message) + binding.componentView.isVisible = false + binding.progressIndicator.isVisible = false + binding.googlePayButton.isVisible = false + } + + GooglePayViewState.Loading -> { + binding.errorView.isVisible = false + binding.componentView.isVisible = false + binding.progressIndicator.isVisible = true + binding.googlePayButton.isVisible = false + } + + GooglePayViewState.ShowButton -> { + binding.errorView.isVisible = false + binding.componentView.isVisible = false + binding.progressIndicator.isVisible = false + binding.googlePayButton.isVisible = true + } + + GooglePayViewState.ShowComponent -> { + binding.errorView.isVisible = false + binding.componentView.isVisible = true + binding.progressIndicator.isVisible = false + binding.googlePayButton.isVisible = false + } + } + } + + private fun onEvent(event: GooglePayEvent) { + when (event) { + is GooglePayEvent.AdditionalAction -> googlePayComponent?.handleAction(event.action, requireActivity()) + is GooglePayEvent.PaymentResult -> onPaymentResult(event.result) + } + } + + private fun onPaymentResult(result: String) { + Toast.makeText(requireContext(), result, Toast.LENGTH_SHORT).show() + dismiss() + } + + private fun loadGooglePayButton() { + val allowedPaymentMethods = googlePayComponent?.getGooglePayButtonParameters()?.allowedPaymentMethods.orEmpty() + val buttonOptions = ButtonOptions + .newBuilder() + .setButtonType(ButtonType.PAY) + .setAllowedPaymentMethods(allowedPaymentMethods) + .build() + binding.googlePayButton.initialize(buttonOptions) + + binding.googlePayButton.setOnClickListener { + googlePayComponent?.startGooglePayScreen(requireActivity(), ACTIVITY_RESULT_CODE) + } + } + + // It is required to use onActivityResult with the Google Pay library (AutoResolveHelper). + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == ACTIVITY_RESULT_CODE) { + googlePayComponent?.handleActivityResult(resultCode, data) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + googlePayComponent = null + } + + companion object { + + internal val TAG = getLogTag() + + internal const val RETURN_URL_EXTRA = "RETURN_URL_EXTRA" + internal const val ACTIVITY_RESULT_CODE = 1 + + fun show(fragmentManager: FragmentManager) { + GooglePayFragment().show(fragmentManager, TAG) + } + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt new file mode 100644 index 0000000000..92d4b6d03c --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewModel.kt @@ -0,0 +1,186 @@ +/* + * 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/8/2023. + */ + +package com.adyen.checkout.example.ui.googlepay + +import android.app.Application +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.adyen.checkout.components.core.ActionComponentData +import com.adyen.checkout.components.core.ComponentAvailableCallback +import com.adyen.checkout.components.core.ComponentCallback +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.PaymentComponentData +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.example.R +import com.adyen.checkout.example.data.storage.KeyValueStorage +import com.adyen.checkout.example.repositories.PaymentsRepository +import com.adyen.checkout.example.service.createPaymentRequest +import com.adyen.checkout.example.service.getPaymentMethodRequest +import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider +import com.adyen.checkout.googlepay.GooglePayComponent +import com.adyen.checkout.googlepay.GooglePayComponentState +import com.adyen.checkout.googlepay.GooglePayConfiguration +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject +import javax.inject.Inject + +@Suppress("TooManyFunctions") +@HiltViewModel +internal class GooglePayViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val application: Application, + private val paymentsRepository: PaymentsRepository, + private val keyValueStorage: KeyValueStorage, + checkoutConfigurationProvider: CheckoutConfigurationProvider, +) : ViewModel(), + ComponentCallback, + ComponentAvailableCallback { + + private val googlePayConfiguration = checkoutConfigurationProvider.getGooglePayConfiguration() + + private val _googleComponentDataFlow = MutableStateFlow(null) + val googleComponentDataFlow: Flow = _googleComponentDataFlow.filterNotNull() + + private val _viewState = MutableStateFlow(GooglePayViewState.Loading) + val viewState: Flow = _viewState + + private val _events = MutableSharedFlow() + val events: Flow = _events + + init { + viewModelScope.launch { fetchPaymentMethods() } + } + + private suspend fun fetchPaymentMethods() = withContext(Dispatchers.IO) { + val paymentMethodResponse = paymentsRepository.getPaymentMethods( + getPaymentMethodRequest( + merchantAccount = keyValueStorage.getMerchantAccount(), + shopperReference = keyValueStorage.getShopperReference(), + amount = keyValueStorage.getAmount(), + countryCode = keyValueStorage.getCountry(), + shopperLocale = keyValueStorage.getShopperLocale(), + splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), + ), + ) + + val paymentMethod = paymentMethodResponse + ?.paymentMethods + ?.firstOrNull { GooglePayComponent.PROVIDER.isPaymentMethodSupported(it) } + + if (paymentMethod == null) { + _viewState.emit(GooglePayViewState.Error(R.string.error_dialog_title)) + return@withContext + } + + _googleComponentDataFlow.emit( + GooglePayComponentData( + paymentMethod, + googlePayConfiguration, + this@GooglePayViewModel, + ), + ) + + checkGooglePayAvailability(paymentMethod, googlePayConfiguration) + } + + private fun checkGooglePayAvailability( + paymentMethod: PaymentMethod, + googlePayConfiguration: GooglePayConfiguration, + ) { + GooglePayComponent.PROVIDER.isAvailable( + application, + paymentMethod, + googlePayConfiguration, + this, + ) + } + + override fun onAvailabilityResult(isAvailable: Boolean, paymentMethod: PaymentMethod) { + viewModelScope.launch { + if (isAvailable) { + _viewState.emit(GooglePayViewState.ShowButton) + } else { + _viewState.emit(GooglePayViewState.Error(R.string.google_pay_unavailable_error)) + } + } + } + + override fun onSubmit(state: GooglePayComponentState) { + makePayment(state.data) + } + + override fun onAdditionalDetails(actionComponentData: ActionComponentData) { + sendPaymentDetails(actionComponentData) + } + + override fun onError(componentError: ComponentError) { + onComponentError(componentError) + } + + private fun onComponentError(error: ComponentError) { + viewModelScope.launch { _events.emit(GooglePayEvent.PaymentResult("Failed: ${error.errorMessage}")) } + } + + private fun sendPaymentDetails(actionComponentData: ActionComponentData) { + viewModelScope.launch(Dispatchers.IO) { + val json = ActionComponentData.SERIALIZER.serialize(actionComponentData) + handlePaymentResponse(paymentsRepository.makeDetailsRequest(json)) + } + } + + private suspend fun handlePaymentResponse(json: JSONObject?) { + json?.let { + when { + json.has("action") -> { + val action = Action.SERIALIZER.deserialize(json.getJSONObject("action")) + handleAction(action) + } + + else -> _events.emit(GooglePayEvent.PaymentResult("Finished: ${json.optString("resultCode")}")) + } + } ?: _events.emit(GooglePayEvent.PaymentResult("Failed")) + } + + private suspend fun handleAction(action: Action) { + _viewState.emit(GooglePayViewState.ShowComponent) + _events.emit(GooglePayEvent.AdditionalAction(action)) + } + + private fun makePayment(data: PaymentComponentData<*>) { + _viewState.value = GooglePayViewState.Loading + + val paymentComponentData = PaymentComponentData.SERIALIZER.serialize(data) + + viewModelScope.launch(Dispatchers.IO) { + val paymentRequest = createPaymentRequest( + paymentComponentData = paymentComponentData, + shopperReference = keyValueStorage.getShopperReference(), + amount = keyValueStorage.getAmount(), + countryCode = keyValueStorage.getCountry(), + merchantAccount = keyValueStorage.getMerchantAccount(), + redirectUrl = savedStateHandle.get(GooglePayFragment.RETURN_URL_EXTRA) + ?: error("Return url should be set"), + isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + shopperEmail = keyValueStorage.getShopperEmail(), + ) + + handlePaymentResponse(paymentsRepository.makePaymentsRequest(paymentRequest)) + } + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewState.kt new file mode 100644 index 0000000000..d9bceb0940 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/GooglePayViewState.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 josephj on 17/8/2023. + */ + +package com.adyen.checkout.example.ui.googlepay + +import androidx.annotation.StringRes + +internal sealed class GooglePayViewState { + + data object Loading : GooglePayViewState() + + data object ShowButton : GooglePayViewState() + + data object ShowComponent : GooglePayViewState() + + data class Error(@StringRes val message: Int) : GooglePayViewState() +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayAction.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayAction.kt new file mode 100644 index 0000000000..7abb0433eb --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayAction.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 josephj on 20/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import com.adyen.checkout.components.core.action.Action + +internal data class SessionsGooglePayAction( + val componentData: SessionsGooglePayComponentData, + val action: Action, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayActivity.kt new file mode 100644 index 0000000000..756fb23a5a --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayActivity.kt @@ -0,0 +1,72 @@ +/* + * 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 13/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import com.adyen.checkout.example.ui.theme.ExampleTheme +import com.adyen.checkout.example.ui.theme.NightThemeRepository +import com.adyen.checkout.redirect.RedirectComponent +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class SessionsGooglePayActivity : AppCompatActivity() { + + @Inject + internal lateinit var nightThemeRepository: NightThemeRepository + + private val sessionsGooglePayViewModel: SessionsGooglePayViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Helps to resize the view port when the keyboard is displayed. + WindowCompat.setDecorFitsSystemWindows(window, false) + + // Insert return url in extras, so we can access it in the ViewModel through SavedStateHandle + val returnUrl = RedirectComponent.getReturnUrl(applicationContext) + "/sessions/googlepay" + intent = (intent ?: Intent()).putExtra(RETURN_URL_EXTRA, returnUrl) + + setContent { + val isDarkTheme = nightThemeRepository.isDarkTheme() + ExampleTheme(isDarkTheme) { + SessionsGooglePayScreen( + useDarkTheme = isDarkTheme, + onBackPressed = { onBackPressedDispatcher.onBackPressed() }, + viewModel = sessionsGooglePayViewModel, + ) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + val data = intent.data + if (data != null && data.toString().startsWith(RedirectComponent.REDIRECT_RESULT_SCHEME)) { + sessionsGooglePayViewModel.onNewIntent(intent) + } + } + + // It is required to use onActivityResult with the Google Pay library (AutoResolveHelper). + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + sessionsGooglePayViewModel.onActivityResult(requestCode, resultCode, data) + } + + companion object { + internal const val RETURN_URL_EXTRA = "RETURN_URL_EXTRA" + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.kt new file mode 100644 index 0000000000..8e46b43d97 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayComponentData.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 josephj on 13/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.googlepay.GooglePayComponentState +import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.sessions.core.CheckoutSession +import com.adyen.checkout.sessions.core.SessionComponentCallback + +internal data class SessionsGooglePayComponentData( + val checkoutSession: CheckoutSession, + val googlePayConfiguration: GooglePayConfiguration, + val paymentMethod: PaymentMethod, + val callback: SessionComponentCallback +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayIntent.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayIntent.kt new file mode 100644 index 0000000000..92b71e8488 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayIntent.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 josephj on 20/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import android.content.Intent + +internal data class SessionsGooglePayIntent( + val componentData: SessionsGooglePayComponentData, + val intent: Intent, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt new file mode 100644 index 0000000000..d21bb52097 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayScreen.kt @@ -0,0 +1,191 @@ +/* + * 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 13/12/2023. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.adyen.checkout.example.ui.googlepay.compose + +import android.app.Activity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.adyen.checkout.components.compose.AdyenComponent +import com.adyen.checkout.components.compose.get +import com.adyen.checkout.example.ui.compose.ResultContent +import com.adyen.checkout.example.ui.googlepay.GooglePayActivityResult +import com.adyen.checkout.googlepay.GooglePayComponent +import com.google.pay.button.ButtonTheme +import com.google.pay.button.ButtonType +import com.google.pay.button.PayButton + +@Composable +internal fun SessionsGooglePayScreen( + useDarkTheme: Boolean, + onBackPressed: () -> Unit, + viewModel: SessionsGooglePayViewModel, +) { + Scaffold( + modifier = Modifier.windowInsetsPadding(WindowInsets.ime), + topBar = { + TopAppBar( + title = { Text(text = "Google Pay with sessions") }, + navigationIcon = { + IconButton(onClick = onBackPressed) { + Icon(Icons.Filled.ArrowBack, contentDescription = "Back") + } + }, + ) + }, + ) { innerPadding -> + val googlePayState by viewModel.googlePayState.collectAsState() + SessionsGooglePayContent( + googlePayState = googlePayState, + onButtonClicked = viewModel::onButtonClicked, + useDarkTheme = useDarkTheme, + modifier = Modifier.padding(innerPadding), + ) + + with(googlePayState) { + HandleStartGooglePay(startGooglePay, viewModel::onGooglePayStarted) + HandleActivityResult(activityResultToHandle, viewModel::onActivityResultHandled) + HandleAction(actionToHandle, viewModel::onActionConsumed) + HandleNewIntent(intentToHandle, viewModel::onNewIntentHandled) + } + } +} + +@Suppress("LongParameterList") +@Composable +private fun SessionsGooglePayContent( + googlePayState: SessionsGooglePayState, + onButtonClicked: () -> Unit, + useDarkTheme: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when (val uiState = googlePayState.uiState) { + SessionsGooglePayUIState.Loading -> { + CircularProgressIndicator() + } + + is SessionsGooglePayUIState.ShowButton -> { + val googlePayComponent = getGooglePayComponent(componentData = uiState.componentData) + PayButton( + onClick = onButtonClicked, + allowedPaymentMethods = googlePayComponent.getGooglePayButtonParameters().allowedPaymentMethods, + theme = if (useDarkTheme) ButtonTheme.Light else ButtonTheme.Dark, + type = ButtonType.Pay, + ) + } + + is SessionsGooglePayUIState.ShowComponent -> { + val googlePayComponent = getGooglePayComponent(componentData = uiState.componentData) + AdyenComponent( + googlePayComponent, + modifier, + ) + } + + is SessionsGooglePayUIState.FinalResult -> { + ResultContent(uiState.finalResult) + } + } + } +} + +@Composable +private fun HandleStartGooglePay( + startGooglePayData: SessionsStartGooglePayData?, + onGooglePayStarted: () -> Unit +) { + if (startGooglePayData == null) return + val activity = LocalContext.current as Activity + val googlePayComponent = getGooglePayComponent(componentData = startGooglePayData.componentData) + LaunchedEffect(startGooglePayData) { + googlePayComponent.startGooglePayScreen( + activity, + startGooglePayData.requestCode, + ) + onGooglePayStarted() + } +} + +@Composable +private fun HandleActivityResult( + activityResultToHandle: GooglePayActivityResult?, + onActivityResultHandled: () -> Unit +) { + if (activityResultToHandle == null) return + val googlePayComponent = getGooglePayComponent(componentData = activityResultToHandle.componentData) + LaunchedEffect(activityResultToHandle) { + googlePayComponent.handleActivityResult(activityResultToHandle.resultCode, activityResultToHandle.data) + onActivityResultHandled() + } +} + +@Composable +private fun HandleAction( + actionToHandle: SessionsGooglePayAction?, + onActionConsumed: () -> Unit +) { + if (actionToHandle == null) return + val activity = LocalContext.current as Activity + val googlePayComponent = getGooglePayComponent(componentData = actionToHandle.componentData) + LaunchedEffect(actionToHandle) { + googlePayComponent.handleAction(actionToHandle.action, activity) + onActionConsumed() + } +} + +@Composable +private fun HandleNewIntent( + intentToHandle: SessionsGooglePayIntent?, + onNewIntentHandled: () -> Unit +) { + if (intentToHandle == null) return + val googlePayComponent = getGooglePayComponent(componentData = intentToHandle.componentData) + LaunchedEffect(intentToHandle) { + googlePayComponent.handleIntent(intentToHandle.intent) + onNewIntentHandled() + } +} + +@Composable +private fun getGooglePayComponent(componentData: SessionsGooglePayComponentData): GooglePayComponent { + return with(componentData) { + GooglePayComponent.PROVIDER.get( + checkoutSession, + paymentMethod, + googlePayConfiguration, + callback, + hashCode().toString(), + ) + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayState.kt new file mode 100644 index 0000000000..af6755c00c --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayState.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 josephj on 13/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import androidx.compose.runtime.Immutable +import com.adyen.checkout.example.ui.googlepay.GooglePayActivityResult + +@Immutable +internal data class SessionsGooglePayState( + val uiState: SessionsGooglePayUIState, + val startGooglePay: SessionsStartGooglePayData? = null, + val activityResultToHandle: GooglePayActivityResult? = null, + val actionToHandle: SessionsGooglePayAction? = null, + val intentToHandle: SessionsGooglePayIntent? = null, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayUIState.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayUIState.kt new file mode 100644 index 0000000000..9e9b9e0b99 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayUIState.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 josephj on 13/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import androidx.compose.runtime.Immutable +import com.adyen.checkout.example.ui.compose.ResultState + +internal sealed class SessionsGooglePayUIState { + + @Immutable + data object Loading : SessionsGooglePayUIState() + + @Immutable + data class ShowButton(val componentData: SessionsGooglePayComponentData) : SessionsGooglePayUIState() + + @Immutable + data class ShowComponent(val componentData: SessionsGooglePayComponentData) : SessionsGooglePayUIState() + + @Immutable + data class FinalResult(val finalResult: ResultState) : SessionsGooglePayUIState() +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt new file mode 100644 index 0000000000..8b7d184ef6 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsGooglePayViewModel.kt @@ -0,0 +1,223 @@ +/* + * 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 13/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +import android.app.Application +import android.content.Intent +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.adyen.checkout.components.core.ComponentAvailableCallback +import com.adyen.checkout.components.core.ComponentError +import com.adyen.checkout.components.core.PaymentMethod +import com.adyen.checkout.components.core.PaymentMethodTypes +import com.adyen.checkout.components.core.action.Action +import com.adyen.checkout.example.data.storage.KeyValueStorage +import com.adyen.checkout.example.extensions.getLogTag +import com.adyen.checkout.example.repositories.PaymentsRepository +import com.adyen.checkout.example.service.getSessionRequest +import com.adyen.checkout.example.service.getSettingsInstallmentOptionsMode +import com.adyen.checkout.example.ui.compose.ResultState +import com.adyen.checkout.example.ui.configuration.CheckoutConfigurationProvider +import com.adyen.checkout.example.ui.googlepay.GooglePayActivityResult +import com.adyen.checkout.googlepay.GooglePayComponent +import com.adyen.checkout.googlepay.GooglePayComponentState +import com.adyen.checkout.googlepay.GooglePayConfiguration +import com.adyen.checkout.sessions.core.CheckoutSession +import com.adyen.checkout.sessions.core.CheckoutSessionProvider +import com.adyen.checkout.sessions.core.CheckoutSessionResult +import com.adyen.checkout.sessions.core.SessionComponentCallback +import com.adyen.checkout.sessions.core.SessionModel +import com.adyen.checkout.sessions.core.SessionPaymentResult +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@Suppress("TooManyFunctions") +@HiltViewModel +internal class SessionsGooglePayViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val application: Application, + private val paymentsRepository: PaymentsRepository, + private val keyValueStorage: KeyValueStorage, + checkoutConfigurationProvider: CheckoutConfigurationProvider, +) : ViewModel(), + SessionComponentCallback, + ComponentAvailableCallback { + + private val googlePayConfiguration = checkoutConfigurationProvider.getGooglePayConfiguration() + + private val _googlePayState = MutableStateFlow(SessionsGooglePayState(SessionsGooglePayUIState.Loading)) + val googlePayState: StateFlow = _googlePayState.asStateFlow() + + private var _componentData: SessionsGooglePayComponentData? = null + private val componentData: SessionsGooglePayComponentData + get() = requireNotNull(_componentData) { "component data should not be null" } + + init { + viewModelScope.launch { fetchSession() } + } + + private suspend fun fetchSession() = withContext(Dispatchers.IO) { + val paymentMethodType = PaymentMethodTypes.GOOGLE_PAY + val checkoutSession = getSession(paymentMethodType) + if (checkoutSession == null) { + Log.e(TAG, "Failed to fetch session") + onError() + return@withContext + } + val paymentMethod = checkoutSession.getPaymentMethod(paymentMethodType) + if (paymentMethod == null) { + Log.e(TAG, "Session does not contain SCHEME payment method") + onError() + return@withContext + } + + _componentData = SessionsGooglePayComponentData( + checkoutSession, + googlePayConfiguration, + paymentMethod, + this@SessionsGooglePayViewModel, + ) + + checkGooglePayAvailability(paymentMethod, googlePayConfiguration) + } + + private suspend fun getSession(paymentMethodType: String): CheckoutSession? { + val sessionModel = paymentsRepository.createSession( + getSessionRequest( + merchantAccount = keyValueStorage.getMerchantAccount(), + shopperReference = keyValueStorage.getShopperReference(), + amount = keyValueStorage.getAmount(), + countryCode = keyValueStorage.getCountry(), + shopperLocale = keyValueStorage.getShopperLocale(), + splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), + isExecuteThreeD = keyValueStorage.isExecuteThreeD(), + isThreeds2Enabled = keyValueStorage.isThreeds2Enabled(), + redirectUrl = savedStateHandle.get(SessionsGooglePayActivity.RETURN_URL_EXTRA) + ?: error("Return url should be set"), + shopperEmail = keyValueStorage.getShopperEmail(), + allowedPaymentMethods = listOf(paymentMethodType), + installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + ), + ) ?: return null + + return getCheckoutSession(sessionModel, googlePayConfiguration) + } + + private suspend fun getCheckoutSession( + sessionModel: SessionModel, + googlePayConfiguration: GooglePayConfiguration, + ): CheckoutSession? { + return when (val result = CheckoutSessionProvider.createSession(sessionModel, googlePayConfiguration)) { + is CheckoutSessionResult.Success -> result.checkoutSession + is CheckoutSessionResult.Error -> null + } + } + + private fun checkGooglePayAvailability( + paymentMethod: PaymentMethod, + googlePayConfiguration: GooglePayConfiguration, + ) { + GooglePayComponent.PROVIDER.isAvailable( + application, + paymentMethod, + googlePayConfiguration, + this, + ) + } + + override fun onAvailabilityResult(isAvailable: Boolean, paymentMethod: PaymentMethod) { + viewModelScope.launch { + if (isAvailable) { + updateState { it.copy(uiState = SessionsGooglePayUIState.ShowButton(componentData)) } + } else { + onError() + } + } + } + + override fun onAction(action: Action) { + updateState { it.copy(actionToHandle = SessionsGooglePayAction(componentData, action)) } + } + + override fun onError(componentError: ComponentError) { + Log.e(TAG, "Component error occurred") + onError() + } + + override fun onFinished(result: SessionPaymentResult) { + updateState { + it.copy(uiState = SessionsGooglePayUIState.FinalResult(getFinalResultState(result))) + } + } + + private fun getFinalResultState(result: SessionPaymentResult): ResultState = when (result.resultCode) { + "Authorised" -> ResultState.SUCCESS + "Pending", + "Received" -> ResultState.PENDING + + else -> ResultState.FAILURE + } + + private fun onError() { + updateState { it.copy(uiState = SessionsGooglePayUIState.FinalResult(ResultState.FAILURE)) } + } + + private fun updateState(block: (SessionsGooglePayState) -> SessionsGooglePayState) { + _googlePayState.update(block) + } + + fun onButtonClicked() { + updateState { + it.copy( + uiState = SessionsGooglePayUIState.ShowComponent(componentData), + startGooglePay = SessionsStartGooglePayData(componentData, ACTIVITY_RESULT_CODE), + ) + } + } + + fun onGooglePayStarted() { + updateState { it.copy(startGooglePay = null) } + } + + fun onActionConsumed() { + updateState { it.copy(actionToHandle = null) } + } + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode != ACTIVITY_RESULT_CODE) return + updateState { it.copy(activityResultToHandle = GooglePayActivityResult(componentData, resultCode, data)) } + } + + fun onActivityResultHandled() { + updateState { it.copy(activityResultToHandle = null) } + } + + fun onNewIntent(intent: Intent) { + updateState { it.copy(intentToHandle = SessionsGooglePayIntent(componentData, intent)) } + } + + fun onNewIntentHandled() { + updateState { it.copy(intentToHandle = null) } + } + + companion object { + private val TAG = getLogTag() + private const val ACTIVITY_RESULT_CODE = 1 + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsStartGooglePayData.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsStartGooglePayData.kt new file mode 100644 index 0000000000..700a248e62 --- /dev/null +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/googlepay/compose/SessionsStartGooglePayData.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 josephj on 20/12/2023. + */ + +package com.adyen.checkout.example.ui.googlepay.compose + +internal data class SessionsStartGooglePayData( + val componentData: SessionsGooglePayComponentData, + val requestCode: Int, +) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItem.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItem.kt index 6eb6a1a441..3047762ba8 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItem.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItem.kt @@ -31,5 +31,7 @@ internal sealed class ComponentItem { object CardWithSessionTakenOver : Entry(R.string.card_component_with_session_taken_over_entry) object GiftCard : Entry(R.string.gift_card_component_entry) object GiftCardWithSession : Entry(R.string.gift_card_with_session_component_entry) + object GooglePay : Entry(R.string.google_pay_component_entry) + object GooglePayWithSession : Entry(R.string.google_pay_with_session_component_entry) } } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItemProvider.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItemProvider.kt index ff3d377acd..30c58a794e 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItemProvider.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/ComponentItemProvider.kt @@ -12,6 +12,7 @@ internal object ComponentItemProvider { ComponentItem.Entry.Blik, ComponentItem.Entry.Card, ComponentItem.Entry.GiftCard, + ComponentItem.Entry.GooglePay, ComponentItem.Entry.Klarna, ComponentItem.Entry.PayPal, ComponentItem.Entry.Instant(instantPaymentMethodType), @@ -24,6 +25,7 @@ internal object ComponentItemProvider { ComponentItem.Title(R.string.components_title), ComponentItem.Entry.CardWithSession, ComponentItem.Entry.CardWithSessionTakenOver, + ComponentItem.Entry.GooglePayWithSession, ComponentItem.Entry.GiftCardWithSession, ) } diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt index e733e6fa24..8d50b61db1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainActivity.kt @@ -35,6 +35,8 @@ import com.adyen.checkout.example.ui.card.compose.SessionsCardActivity import com.adyen.checkout.example.ui.configuration.ConfigurationActivity import com.adyen.checkout.example.ui.giftcard.GiftCardActivity import com.adyen.checkout.example.ui.giftcard.SessionsGiftCardActivity +import com.adyen.checkout.example.ui.googlepay.GooglePayFragment +import com.adyen.checkout.example.ui.googlepay.compose.SessionsGooglePayActivity import com.adyen.checkout.example.ui.instant.InstantFragment import com.adyen.checkout.redirect.RedirectComponent import dagger.hilt.android.AndroidEntryPoint @@ -135,6 +137,7 @@ class MainActivity : AppCompatActivity() { } } + @Suppress("LongMethod") private fun onNavigateTo(navigation: MainNavigation) { when (navigation) { is MainNavigation.DropIn -> { @@ -166,40 +169,45 @@ class MainActivity : AppCompatActivity() { ) } - is MainNavigation.Bacs -> BacsFragment.show(supportFragmentManager) + is MainNavigation.Bacs -> { + BacsFragment.show(supportFragmentManager) + } + is MainNavigation.Blik -> { - val intent = Intent(this, BlikActivity::class.java) - startActivity(intent) + startActivity(Intent(this, BlikActivity::class.java)) } - MainNavigation.Card -> { - val intent = Intent(this, CardActivity::class.java) - startActivity(intent) + is MainNavigation.Card -> { + startActivity(Intent(this, CardActivity::class.java)) } - MainNavigation.CardWithSession -> { - val intent = Intent(this, SessionsCardActivity::class.java) - startActivity(intent) + is MainNavigation.CardWithSession -> { + startActivity(Intent(this, SessionsCardActivity::class.java)) } - MainNavigation.GiftCard -> { - val intent = Intent(this, GiftCardActivity::class.java) - startActivity(intent) + is MainNavigation.GiftCard -> { + startActivity(Intent(this, GiftCardActivity::class.java)) } - MainNavigation.GiftCardWithSession -> { - val intent = Intent(this, SessionsGiftCardActivity::class.java) - startActivity(intent) + is MainNavigation.GiftCardWithSession -> { + startActivity(Intent(this, SessionsGiftCardActivity::class.java)) } - MainNavigation.CardWithSessionTakenOver -> { - val intent = Intent(this, SessionsCardTakenOverActivity::class.java) - startActivity(intent) + is MainNavigation.CardWithSessionTakenOver -> { + startActivity(Intent(this, SessionsCardTakenOverActivity::class.java)) } is MainNavigation.Instant -> { InstantFragment.show(supportFragmentManager, navigation.paymentMethodType) } + + is MainNavigation.GooglePay -> { + GooglePayFragment.show(supportFragmentManager) + } + + is MainNavigation.GooglePayWithSession -> { + startActivity(Intent(this, SessionsGooglePayActivity::class.java)) + } } } @@ -215,6 +223,16 @@ class MainActivity : AppCompatActivity() { binding.switchSessions.isChecked = isChecked } + // It is required to use onActivityResult with the Google Pay library (AutoResolveHelper). + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == GooglePayFragment.ACTIVITY_RESULT_CODE) { + (supportFragmentManager.findFragmentByTag(GooglePayFragment.TAG) as? GooglePayFragment) + ?.onActivityResult(requestCode, resultCode, data) + } + } + override fun onDestroy() { super.onDestroy() componentItemAdapter = null diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt index 94891df843..e72a8d6ee9 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainNavigation.kt @@ -14,21 +14,25 @@ import com.adyen.checkout.sessions.core.CheckoutSession internal sealed class MainNavigation { - object Bacs : MainNavigation() + data object Bacs : MainNavigation() - object Blik : MainNavigation() + data object Blik : MainNavigation() - object Card : MainNavigation() + data object Card : MainNavigation() - class Instant(val paymentMethodType: String) : MainNavigation() + data class Instant(val paymentMethodType: String) : MainNavigation() - object CardWithSession : MainNavigation() + data object CardWithSession : MainNavigation() - object GiftCard : MainNavigation() + data object GiftCard : MainNavigation() - object GiftCardWithSession : MainNavigation() + data object GiftCardWithSession : MainNavigation() - object CardWithSessionTakenOver : MainNavigation() + data object CardWithSessionTakenOver : MainNavigation() + + data object GooglePay : MainNavigation() + + data object GooglePayWithSession : MainNavigation() data class DropIn( val paymentMethodsApiResponse: PaymentMethodsApiResponse, diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt index 001f068f0d..980ebc28cf 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/main/MainViewModel.kt @@ -30,8 +30,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import javax.inject.Inject @@ -44,38 +43,48 @@ internal class MainViewModel @Inject constructor( private val checkoutConfigurationProvider: CheckoutConfigurationProvider, ) : ViewModel() { + private val lifecycleResumed: MutableSharedFlow = MutableSharedFlow() private val useSessions: MutableStateFlow = MutableStateFlow(keyValueStorage.useSessions()) private val showLoading: MutableStateFlow = MutableStateFlow(false) - private val _mainViewState: MutableStateFlow = MutableStateFlow(getViewState()) + private val _mainViewState: MutableStateFlow = MutableStateFlow(getInitialViewState()) val mainViewState: Flow = _mainViewState private val _eventFlow: MutableSharedFlow = MutableSharedFlow(extraBufferCapacity = 1) val eventFlow: Flow = _eventFlow init { - useSessions.onEach { - loadViewState() - }.launchIn(viewModelScope) + viewModelScope.launch { + combineViewStateFlows() + } + } - showLoading.onEach { - loadViewState() - }.launchIn(viewModelScope) + private suspend fun combineViewStateFlows() { + combine( + lifecycleResumed, + useSessions, + showLoading, + ) { _, useSessions, showLoading -> + getViewState(useSessions, showLoading) + }.collect { + loadViewState(it) + } } internal fun onResume() { viewModelScope.launch { - loadViewState() + lifecycleResumed.emit(Unit) } } + @Suppress("CyclomaticComplexMethod") fun onComponentEntryClick(entry: ComponentItem.Entry) { when (entry) { is ComponentItem.Entry.Bacs -> _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.Bacs)) is ComponentItem.Entry.Blik -> _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.Blik)) is ComponentItem.Entry.Card -> _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.Card)) is ComponentItem.Entry.Klarna -> _eventFlow.tryEmit( - MainEvent.NavigateTo(MainNavigation.Instant(PAYMENT_METHOD_KLARNA)) + MainEvent.NavigateTo(MainNavigation.Instant(PAYMENT_METHOD_KLARNA)), ) is ComponentItem.Entry.PayPal -> @@ -83,7 +92,7 @@ internal class MainViewModel @Inject constructor( is ComponentItem.Entry.Instant -> _eventFlow.tryEmit( - MainEvent.NavigateTo(MainNavigation.Instant(keyValueStorage.getInstantPaymentMethodType())) + MainEvent.NavigateTo(MainNavigation.Instant(keyValueStorage.getInstantPaymentMethodType())), ) is ComponentItem.Entry.CardWithSession -> @@ -96,6 +105,11 @@ internal class MainViewModel @Inject constructor( is ComponentItem.Entry.GiftCardWithSession -> _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.GiftCardWithSession)) + is ComponentItem.Entry.GooglePay -> _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.GooglePay)) + is ComponentItem.Entry.GooglePayWithSession -> { + _eventFlow.tryEmit(MainEvent.NavigateTo(MainNavigation.GooglePayWithSession)) + } + is ComponentItem.Entry.DropIn -> startDropInFlow() is ComponentItem.Entry.DropInWithSession -> startSessionDropInFlow(false) is ComponentItem.Entry.DropInWithCustomSession -> startSessionDropInFlow(true) @@ -150,7 +164,7 @@ internal class MainViewModel @Inject constructor( countryCode = keyValueStorage.getCountry(), shopperLocale = keyValueStorage.getShopperLocale(), splitCardFundingSources = keyValueStorage.isSplitCardFundingSources(), - ) + ), ) private suspend fun getSession(dropInConfiguration: DropInConfiguration): CheckoutSession? { @@ -168,8 +182,8 @@ internal class MainViewModel @Inject constructor( ?: error("Return url should be set"), shopperEmail = keyValueStorage.getShopperEmail(), installmentOptions = getSettingsInstallmentOptionsMode(keyValueStorage.getInstallmentOptionsMode()), - showInstallmentAmount = keyValueStorage.isInstallmentAmountShown() - ) + showInstallmentAmount = keyValueStorage.isInstallmentAmountShown(), + ), ) ?: return null return getCheckoutSession(sessionModel, dropInConfiguration) @@ -203,13 +217,16 @@ internal class MainViewModel @Inject constructor( showLoading.emit(loading) } - private suspend fun loadViewState() { - _mainViewState.emit(getViewState()) - } - - private fun getViewState(): MainViewState { + private fun getInitialViewState(): MainViewState { val useSessions = useSessions.value val showLoading = showLoading.value + return getViewState(useSessions, showLoading) + } + + private fun getViewState( + useSessions: Boolean, + showLoading: Boolean, + ): MainViewState { return MainViewState( listItems = getListItems(useSessions), useSessions = useSessions, @@ -217,6 +234,10 @@ internal class MainViewModel @Inject constructor( ) } + private suspend fun loadViewState(mainViewState: MainViewState) { + _mainViewState.emit(mainViewState) + } + private fun getListItems(useSessions: Boolean): List { return if (useSessions) { ComponentItemProvider.getSessionItems() diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt index b1e443d483..866fdc6355 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/theme/NightThemeRepository.kt @@ -8,8 +8,12 @@ package com.adyen.checkout.example.ui.theme +import android.content.Context import android.content.SharedPreferences +import android.content.res.Configuration import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable import androidx.core.content.edit import javax.inject.Inject import javax.inject.Singleton @@ -19,6 +23,11 @@ internal interface NightThemeRepository { var theme: NightTheme fun initialize() + + @Composable + fun isDarkTheme(): Boolean + + fun isDarkTheme(context: Context): Boolean } @Singleton @@ -42,6 +51,30 @@ internal class DefaultNightThemeRepository @Inject constructor( return NightTheme.findByPreferenceValue(preference) } + @Composable + override fun isDarkTheme(): Boolean { + return when (theme) { + NightTheme.DAY -> false + NightTheme.NIGHT -> true + NightTheme.SYSTEM -> isSystemInDarkTheme() + } + } + + override fun isDarkTheme(context: Context): Boolean { + return when (theme) { + NightTheme.DAY -> false + NightTheme.NIGHT -> true + NightTheme.SYSTEM -> { + when (context.resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) { + Configuration.UI_MODE_NIGHT_YES -> true + Configuration.UI_MODE_NIGHT_NO -> false + Configuration.UI_MODE_NIGHT_UNDEFINED -> false + else -> false + } + } + } + } + companion object { // Should be same as R.string.night_theme_key private const val PREF_KEY_NIGHT_THEME = "night_theme_key" @@ -59,6 +92,6 @@ internal enum class NightTheme( companion object { fun findByPreferenceValue(value: String?): NightTheme = - values().find { it.preferenceValue == value } ?: SYSTEM + entries.find { it.preferenceValue == value } ?: SYSTEM } } diff --git a/example-app/src/main/res/layout/fragment_google_pay.xml b/example-app/src/main/res/layout/fragment_google_pay.xml new file mode 100644 index 0000000000..fa75802986 --- /dev/null +++ b/example-app/src/main/res/layout/fragment_google_pay.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + diff --git a/example-app/src/main/res/values-night/styles.xml b/example-app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..b8704d3158 --- /dev/null +++ b/example-app/src/main/res/values-night/styles.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index 16fe881743..d182683477 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ Your country code must be %s Your currency code must be %s + Google Pay is unavailable on this device Blik Waiting for approval @@ -24,6 +25,8 @@ Klarna PayPal Instant (%s) + Google Pay + Google Pay Use sessions diff --git a/example-app/src/main/res/values/styles.xml b/example-app/src/main/res/values/styles.xml index 002e6393fa..82f8242747 100644 --- a/example-app/src/main/res/values/styles.xml +++ b/example-app/src/main/res/values/styles.xml @@ -38,4 +38,8 @@ + diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt index 346dcc54b4..3346453c19 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/provider/GiftCardComponentProvider.kt @@ -32,10 +32,7 @@ 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.cse.internal.ClientSideEncrypter -import com.adyen.checkout.cse.internal.DateGenerator -import com.adyen.checkout.cse.internal.DefaultCardEncrypter -import com.adyen.checkout.cse.internal.DefaultGenericEncrypter +import com.adyen.checkout.cse.internal.CardEncryptorFactory import com.adyen.checkout.giftcard.GiftCardComponent import com.adyen.checkout.giftcard.GiftCardComponentCallback import com.adyen.checkout.giftcard.GiftCardComponentState @@ -90,10 +87,7 @@ constructor( ): GiftCardComponent { assertSupported(paymentMethod) - val clientSideEncrypter = ClientSideEncrypter() - val dateGenerator = DateGenerator() - val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) - val cardEncrypter = DefaultCardEncrypter(genericEncrypter) + val cardEncryptor = CardEncryptorFactory.provide() val giftCardFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> val componentParams = componentParamsMapper.mapToParams(configuration, null) val httpClient = HttpClientFactory.getHttpClient(componentParams.environment) @@ -118,7 +112,7 @@ constructor( analyticsRepository = analyticsRepository, publicKeyRepository = DefaultPublicKeyRepository(publicKeyService), componentParams = componentParams, - cardEncrypter = cardEncrypter, + cardEncryptor = cardEncryptor, submitHandler = SubmitHandler(savedStateHandle), ) @@ -158,10 +152,7 @@ constructor( ): GiftCardComponent { assertSupported(paymentMethod) - val clientSideEncrypter = ClientSideEncrypter() - val dateGenerator = DateGenerator() - val genericEncrypter = DefaultGenericEncrypter(clientSideEncrypter, dateGenerator) - val cardEncrypter = DefaultCardEncrypter(genericEncrypter) + val cardEncryptor = CardEncryptorFactory.provide() val giftCardFactory = viewModelFactory(savedStateRegistryOwner, null) { savedStateHandle -> val componentParams = componentParamsMapper.mapToParams( configuration = configuration, @@ -190,7 +181,7 @@ constructor( analyticsRepository = analyticsRepository, publicKeyRepository = DefaultPublicKeyRepository(publicKeyService), componentParams = componentParams, - cardEncrypter = cardEncrypter, + cardEncryptor = cardEncryptor, submitHandler = SubmitHandler(savedStateHandle), ) diff --git a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt index 73b6f200d5..5eff1d99b8 100644 --- a/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt +++ b/giftcard/src/main/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegate.kt @@ -32,7 +32,7 @@ import com.adyen.checkout.core.internal.util.Logger import com.adyen.checkout.cse.EncryptedCard import com.adyen.checkout.cse.EncryptionException import com.adyen.checkout.cse.UnencryptedCard -import com.adyen.checkout.cse.internal.BaseCardEncrypter +import com.adyen.checkout.cse.internal.BaseCardEncryptor import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.GiftCardException @@ -63,7 +63,7 @@ internal class DefaultGiftCardDelegate( private val analyticsRepository: AnalyticsRepository, private val publicKeyRepository: PublicKeyRepository, override val componentParams: GiftCardComponentParams, - private val cardEncrypter: BaseCardEncrypter, + private val cardEncryptor: BaseCardEncryptor, private val submitHandler: SubmitHandler, ) : GiftCardDelegate { @@ -249,7 +249,7 @@ internal class DefaultGiftCardDelegate( build() } - cardEncrypter.encryptFields(unencryptedCard, publicKey) + cardEncryptor.encryptFields(unencryptedCard, publicKey) } catch (e: EncryptionException) { exceptionChannel.trySend(e) null diff --git a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt index 466902746a..0b080db0a1 100644 --- a/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt +++ b/giftcard/src/test/java/com/adyen/checkout/giftcard/internal/ui/DefaultGiftCardDelegateTest.kt @@ -17,7 +17,7 @@ import com.adyen.checkout.components.core.internal.PaymentObserverRepository import com.adyen.checkout.components.core.internal.data.api.AnalyticsRepository import com.adyen.checkout.components.core.internal.test.TestPublicKeyRepository import com.adyen.checkout.core.Environment -import com.adyen.checkout.cse.internal.test.TestCardEncrypter +import com.adyen.checkout.cse.internal.test.TestCardEncryptor import com.adyen.checkout.giftcard.GiftCardAction import com.adyen.checkout.giftcard.GiftCardComponentState import com.adyen.checkout.giftcard.GiftCardConfiguration @@ -60,13 +60,13 @@ internal class DefaultGiftCardDelegateTest( @Mock private val submitHandler: SubmitHandler, ) { - private lateinit var cardEncrypter: TestCardEncrypter + private lateinit var cardEncryptor: TestCardEncryptor private lateinit var publicKeyRepository: TestPublicKeyRepository private lateinit var delegate: DefaultGiftCardDelegate @BeforeEach fun before() { - cardEncrypter = TestCardEncrypter() + cardEncryptor = TestCardEncryptor() publicKeyRepository = TestPublicKeyRepository() delegate = createGiftCardDelegate() } @@ -121,7 +121,7 @@ internal class DefaultGiftCardDelegateTest( @Test fun `encryption fails, then component state should be invalid`() = runTest { - cardEncrypter.shouldThrowException = true + cardEncryptor.shouldThrowException = true delegate.initialize(CoroutineScope(UnconfinedTestDispatcher())) @@ -403,7 +403,7 @@ internal class DefaultGiftCardDelegateTest( order = order, publicKeyRepository = publicKeyRepository, componentParams = GiftCardComponentParamsMapper(null, null).mapToParams(configuration, null), - cardEncrypter = cardEncrypter, + cardEncryptor = cardEncryptor, analyticsRepository = analyticsRepository, submitHandler = submitHandler, ) diff --git a/googlepay/build.gradle b/googlepay/build.gradle index 93b0ba364e..f4fd4461b7 100644 --- a/googlepay/build.gradle +++ b/googlepay/build.gradle @@ -41,7 +41,7 @@ dependencies { api project(':sessions-core') // Dependencies - api libraries.playServicesWallet + api libraries.googlePay.playServicesWallet //Tests testImplementation project(':test-core') diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 46ba1da5bd..ad73ab43e2 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -6,7 +6,7 @@ ~ ~ Created by oscars on 16/5/2023. --> - + true false @@ -81,6 +81,14 @@ + + + + + + + + @@ -105,6 +113,14 @@ + + + + + + + + @@ -163,6 +179,14 @@ + + + + + + + + @@ -240,6 +264,14 @@ + + + + + + + + @@ -402,6 +434,14 @@ + + + + + + + + @@ -528,6 +568,14 @@ + + + + + + + + @@ -1271,6 +1319,14 @@ + + + + + + + + @@ -1326,6 +1382,14 @@ + + + + + + + + @@ -1419,6 +1483,14 @@ + + + + + + + + @@ -1459,6 +1531,14 @@ + + + + + + + + @@ -1499,6 +1579,14 @@ + + + + + + + + @@ -1664,6 +1752,14 @@ + + + + + + + + @@ -1680,14 +1776,6 @@ - - - - - - - - @@ -2769,6 +2857,14 @@ + + + + + + + + @@ -2809,6 +2905,14 @@ + + + + + + + + @@ -2834,6 +2938,11 @@ + + + + + @@ -2874,6 +2983,14 @@ + + + + + + + + @@ -2899,6 +3016,11 @@ + + + + + @@ -2939,6 +3061,14 @@ + + + + + + + + @@ -2979,6 +3109,14 @@ + + + + + + + + @@ -3019,6 +3157,14 @@ + + + + + + + + @@ -3059,6 +3205,14 @@ + + + + + + + + @@ -3099,6 +3253,14 @@ + + + + + + + + @@ -3139,6 +3301,14 @@ + + + + + + + + @@ -3179,6 +3349,14 @@ + + + + + + + + @@ -3219,6 +3397,14 @@ + + + + + + + + @@ -3259,6 +3445,14 @@ + + + + + + + + @@ -3299,6 +3493,14 @@ + + + + + + + + @@ -3339,6 +3541,14 @@ + + + + + + + + @@ -3391,6 +3601,17 @@ + + + + + + + + + + + @@ -3431,6 +3652,14 @@ + + + + + + + + @@ -3471,6 +3700,14 @@ + + + + + + + + @@ -3511,6 +3748,14 @@ + + + + + + + + @@ -3551,6 +3796,14 @@ + + + + + + + + @@ -3591,6 +3844,14 @@ + + + + + + + + @@ -3631,6 +3892,14 @@ + + + + + + + + @@ -3671,6 +3940,14 @@ + + + + + + + + @@ -3687,6 +3964,14 @@ + + + + + + + + @@ -3727,6 +4012,14 @@ + + + + + + + + @@ -3767,6 +4060,14 @@ + + + + + + + + @@ -3807,6 +4108,14 @@ + + + + + + + + @@ -3847,6 +4156,14 @@ + + + + + + + + @@ -3911,6 +4228,14 @@ + + + + + + + + @@ -3951,6 +4276,14 @@ + + + + + + + + @@ -3991,6 +4324,14 @@ + + + + + + + + @@ -4031,6 +4372,14 @@ + + + + + + + + @@ -4071,6 +4420,14 @@ + + + + + + + + @@ -4111,6 +4468,14 @@ + + + + + + + + @@ -4151,6 +4516,14 @@ + + + + + + + + @@ -4191,6 +4564,14 @@ + + + + + + + + @@ -4231,6 +4612,14 @@ + + + + + + + + @@ -4271,6 +4660,14 @@ + + + + + + + + @@ -4311,6 +4708,14 @@ + + + + + + + + @@ -4351,6 +4756,14 @@ + + + + + + + + @@ -4391,6 +4804,14 @@ + + + + + + + + @@ -4431,6 +4852,14 @@ + + + + + + + + @@ -4455,6 +4884,14 @@ + + + + + + + + @@ -4495,6 +4932,14 @@ + + + + + + + + @@ -4519,6 +4964,14 @@ + + + + + + + + @@ -4559,6 +5012,14 @@ + + + + + + + + @@ -4599,6 +5060,14 @@ + + + + + + + + @@ -4639,6 +5108,14 @@ + + + + + + + + @@ -4910,6 +5387,14 @@ + + + + + + + + @@ -4931,6 +5416,11 @@ + + + + + @@ -4994,6 +5484,14 @@ + + + + + + + + @@ -5026,6 +5524,14 @@ + + + + + + + + @@ -5058,6 +5564,14 @@ + + + + + + + + @@ -5122,6 +5636,14 @@ + + + + + + + + @@ -5154,6 +5676,14 @@ + + + + + + + + @@ -5186,6 +5716,14 @@ + + + + + + + + @@ -5218,6 +5756,14 @@ + + + + + + + + @@ -5250,6 +5796,14 @@ + + + + + + + + @@ -5270,6 +5824,11 @@ + + + + + @@ -5286,6 +5845,14 @@ + + + + + + + + @@ -5515,6 +6082,14 @@ + + + + + + + + @@ -6804,6 +7379,14 @@ + + + + + + + + @@ -6836,6 +7419,14 @@ + + + + + + + + @@ -6868,6 +7459,14 @@ + + + + + + + + @@ -6900,6 +7499,14 @@ + + + + + + + + @@ -6932,6 +7539,14 @@ + + + + + + + + @@ -6964,6 +7579,14 @@ + + + + + + + + @@ -6996,6 +7619,14 @@ + + + + + + + + @@ -7028,6 +7659,14 @@ + + + + + + + + @@ -7060,6 +7699,14 @@ + + + + + + + + @@ -7092,6 +7739,14 @@ + + + + + + + + @@ -7124,6 +7779,14 @@ + + + + + + + + @@ -7156,6 +7819,14 @@ + + + + + + + + @@ -7188,6 +7859,14 @@ + + + + + + + + @@ -7220,6 +7899,14 @@ + + + + + + + + @@ -7252,6 +7939,14 @@ + + + + + + + + @@ -7284,6 +7979,14 @@ + + + + + + + + @@ -7316,6 +8019,14 @@ + + + + + + + + @@ -7348,6 +8059,14 @@ + + + + + + + + @@ -7380,6 +8099,14 @@ + + + + + + + + @@ -7412,6 +8139,14 @@ + + + + + + + + @@ -7444,6 +8179,14 @@ + + + + + + + + @@ -7476,6 +8219,14 @@ + + + + + + + + @@ -7508,6 +8259,14 @@ + + + + + + + + @@ -7540,6 +8299,14 @@ + + + + + + + + @@ -7572,6 +8339,14 @@ + + + + + + + + @@ -7592,6 +8367,11 @@ + + + + + @@ -7975,6 +8755,14 @@ + + + + + + + + @@ -7988,6 +8776,11 @@ + + + + + @@ -7998,6 +8791,11 @@ + + + + + @@ -8016,6 +8814,14 @@ + + + + + + + + @@ -8040,6 +8846,14 @@ + + + + + + + + @@ -8269,6 +9083,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8798,6 +9689,14 @@ + + + + + + + + @@ -8822,6 +9721,14 @@ + + + + + + + + @@ -8846,6 +9753,14 @@ + + + + + + + + @@ -8866,6 +9781,19 @@ + + + + + + + + + + + + + @@ -8874,6 +9802,14 @@ + + + + + + + + @@ -8882,6 +9818,14 @@ + + + + + + + + @@ -8930,6 +9874,14 @@ + + + + + + + + @@ -8954,6 +9906,14 @@ + + + + + + + + @@ -8978,6 +9938,14 @@ + + + + + + + + @@ -9026,6 +9994,14 @@ + + + + + + + + @@ -9050,6 +10026,14 @@ + + + + + + + + @@ -9074,6 +10058,14 @@ + + + + + + + + @@ -9107,6 +10099,14 @@ + + + + + + + + @@ -9131,6 +10131,14 @@ + + + + + + + + @@ -9155,6 +10163,14 @@ + + + + + + + + @@ -9179,6 +10195,14 @@ + + + + + + + + @@ -9203,6 +10227,14 @@ + + + + + + + + @@ -9227,6 +10259,14 @@ + + + + + + + + @@ -9251,6 +10291,14 @@ + + + + + + + + @@ -9275,6 +10323,14 @@ + + + + + + + + @@ -9299,6 +10355,14 @@ + + + + + + + + @@ -9323,6 +10387,14 @@ + + + + + + + + @@ -9347,6 +10419,14 @@ + + + + + + + + @@ -9443,6 +10523,14 @@ + + + + + + + + @@ -9491,6 +10579,14 @@ + + + + + + + + @@ -9515,6 +10611,14 @@ + + + + + + + + @@ -9539,6 +10643,14 @@ + + + + + + + + @@ -9563,6 +10675,14 @@ + + + + + + + + @@ -9587,6 +10707,14 @@ + + + + + + + + @@ -9690,6 +10818,28 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -9796,6 +10946,24 @@ + + + + + + + + + + + + + + + + + + @@ -9804,6 +10972,14 @@ + + + + + + + + @@ -9884,6 +11060,14 @@ + + + + + + + + @@ -9893,10 +11077,18 @@ + + + + + + + + @@ -10001,6 +11193,14 @@ + + + + + + + + @@ -10025,6 +11225,14 @@ + + + + + + + + @@ -10049,6 +11257,14 @@ + + + + + + + + @@ -10064,6 +11280,11 @@ + + + + + @@ -10151,6 +11372,9 @@ + + + @@ -10561,6 +11785,11 @@ + + + + + @@ -10961,6 +12190,14 @@ + + + + + + + + @@ -10987,6 +12224,11 @@ + + + + + @@ -11005,6 +12247,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49..d64cd49177 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862f7..1af9e0930b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/paybybank/src/main/res/values/styles.xml b/paybybank/src/main/res/values/styles.xml index 62d63f2141..f0b9b1e6b8 100644 --- a/paybybank/src/main/res/values/styles.xml +++ b/paybybank/src/main/res/values/styles.xml @@ -7,6 +7,8 @@ --> + + + + diff --git a/voucher/src/main/res/values/styles.xml b/voucher/src/main/res/values/styles.xml index ef91dfd0a2..32b7fd539f 100644 --- a/voucher/src/main/res/values/styles.xml +++ b/voucher/src/main/res/values/styles.xml @@ -10,6 +10,8 @@