From 51424a4c88de1fb50a973a0421c30dd19704f5a9 Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 6 Mar 2024 17:49:46 +0100 Subject: [PATCH 1/8] Do not use default.local.gradle if local.gradle is not added This forces merchants to copy the default.local.gradle file and replace the placeholders with the actual values COAND-831 --- example-app/build.gradle | 3 +-- example-app/default.local.gradle | 23 ------------------- example-app/example.local.gradle | 23 +++++++++++++++++++ .../example/data/api/CheckoutApiService.kt | 9 -------- .../checkout/example/di/NetworkModule.kt | 10 ++------ 5 files changed, 26 insertions(+), 42 deletions(-) delete mode 100644 example-app/default.local.gradle create mode 100644 example-app/example.local.gradle diff --git a/example-app/build.gradle b/example-app/build.gradle index 6a523c3a96..bcb9b6e49d 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -19,8 +19,7 @@ apply from: "${rootDir}/config/gradle/ci.gradle" if (file("local.gradle").exists()) { apply from: "local.gradle" } else { - logger.lifecycle("File example-app/local.gradle not found. Falling back to default file with no values.") - apply from: "default.local.gradle" + throw new GradleException("File example-app/local.gradle not found. Check example-app/README.md for more instructions.") } // This runConnectedAndroidTest.gradle script is applied, diff --git a/example-app/default.local.gradle b/example-app/default.local.gradle deleted file mode 100644 index 34679f12c5..0000000000 --- a/example-app/default.local.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Replace the values in with the configuration to connect to YOUR Server. The <> should also be removed. - * Your server is the one that should connect to Adyen, DO NOT connect to Adyen directly from the App as this might expose your API keys. - * - * For a permanent local configuration file not tracked by Git, copy this file and rename it to "local.gradle" with your credentials. - */ -android { - buildTypes { - debug { - buildConfigField "String", "MERCHANT_ACCOUNT", "\"\"" - buildConfigField "String", "MERCHANT_SERVER_URL", "\"\"" - buildConfigField "String", "API_KEY_HEADER_NAME", "\"\"" - buildConfigField "String", "CHECKOUT_API_KEY", "\"\"" - buildConfigField "String", "CLIENT_KEY", "\"\"" - buildConfigField "String", "SHOPPER_REFERENCE", "\"\"" - } - - release { - initWith debug - matchingFallbacks = ['debug'] - } - } -} diff --git a/example-app/example.local.gradle b/example-app/example.local.gradle new file mode 100644 index 0000000000..9092957d68 --- /dev/null +++ b/example-app/example.local.gradle @@ -0,0 +1,23 @@ +/** + * Duplicate this file into "local.gradle" then replace the placeholder values with the correct + * parameters. You might need to escape some characters if you see an error. + * + * DO NOT commit the new file anywhere public, you might be exposing your secret credentials. + */ +android { + buildTypes { + debug { + buildConfigField "String", "MERCHANT_SERVER_URL", '"YOUR_SERVER_URL"' + buildConfigField "String", "CLIENT_KEY", '"YOUR_CLIENT_KEY"' + buildConfigField "String", "MERCHANT_ACCOUNT", '"YOUR_MERCHANT_ACCOUNT"' + buildConfigField "String", "API_KEY_HEADER_NAME", '"API_KEY_HEADER"' + buildConfigField "String", "CHECKOUT_API_KEY", '"YOUR_API_KEY"' + buildConfigField "String", "SHOPPER_REFERENCE", '"SHOPPER_REFERENCE"' + } + + release { + initWith debug + matchingFallbacks = ['debug'] + } + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt index 40fe6f11ff..8e694ba992 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt @@ -9,7 +9,6 @@ package com.adyen.checkout.example.data.api import com.adyen.checkout.components.core.PaymentMethodsApiResponse -import com.adyen.checkout.example.BuildConfig import com.adyen.checkout.example.data.api.model.BalanceRequest import com.adyen.checkout.example.data.api.model.CancelOrderRequest import com.adyen.checkout.example.data.api.model.CreateOrderRequest @@ -27,14 +26,6 @@ import retrofit2.http.Query internal interface CheckoutApiService { - companion object { - private const val DEFAULT_GRADLE_SERVER_URL = "" - - fun isRealUrlAvailable(): Boolean { - return BuildConfig.MERCHANT_SERVER_URL != DEFAULT_GRADLE_SERVER_URL - } - } - @POST("sessions") suspend fun sessionsAsync(@Body sessionRequest: SessionRequest): SessionModel diff --git a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt index e4ad77f02f..8be32f7345 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt @@ -29,12 +29,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { - private val BASE_URL = if (CheckoutApiService.isRealUrlAvailable()) { - BuildConfig.MERCHANT_SERVER_URL - } else { - "http://myserver.com/my/endpoint/" - } - @Singleton @Provides internal fun provideOkHttpClient(): OkHttpClient { @@ -63,7 +57,7 @@ object NetworkModule { Moshi.Builder() .add(JSONObjectAdapter()) .add(KotlinJsonAdapterFactory()) - .build() + .build(), ) @Singleton @@ -74,7 +68,7 @@ object NetworkModule { converterFactory: Converter.Factory, ): Retrofit = Retrofit.Builder() - .baseUrl(BASE_URL) + .baseUrl(BuildConfig.MERCHANT_SERVER_URL) .client(okHttpClient) .addConverterFactory(converterFactory) .build() From d075b1b7e3b7ce0a7c8ad4293daed71e38209ad4 Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 6 Mar 2024 17:56:27 +0100 Subject: [PATCH 2/8] Rename API_KEY to AUTHORIZATION_HEADER To make it more generic and not explicitly encourage people to use their API keys COAND-831 --- example-app/example.local.gradle | 4 ++-- .../java/com/adyen/checkout/example/di/NetworkModule.kt | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/example-app/example.local.gradle b/example-app/example.local.gradle index 9092957d68..84245de1c1 100644 --- a/example-app/example.local.gradle +++ b/example-app/example.local.gradle @@ -10,8 +10,8 @@ android { buildConfigField "String", "MERCHANT_SERVER_URL", '"YOUR_SERVER_URL"' buildConfigField "String", "CLIENT_KEY", '"YOUR_CLIENT_KEY"' buildConfigField "String", "MERCHANT_ACCOUNT", '"YOUR_MERCHANT_ACCOUNT"' - buildConfigField "String", "API_KEY_HEADER_NAME", '"API_KEY_HEADER"' - buildConfigField "String", "CHECKOUT_API_KEY", '"YOUR_API_KEY"' + buildConfigField "String", "AUTHORIZATION_HEADER_NAME", '"YOUR_AUTHORIZATION_HEADER_NAME"' + buildConfigField "String", "AUTHORIZATION_HEADER_VALUE", '"YOUR_AUTHORIZATION_HEADER_VALUE"' buildConfigField "String", "SHOPPER_REFERENCE", '"SHOPPER_REFERENCE"' } diff --git a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt index 8be32f7345..52e97c9df8 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt @@ -42,7 +42,11 @@ object NetworkModule { builder.addInterceptor { chain -> val request = chain.request().newBuilder() - .addHeader(BuildConfig.API_KEY_HEADER_NAME, BuildConfig.CHECKOUT_API_KEY) + .apply { + if (BuildConfig.AUTHORIZATION_HEADER_NAME.isNotBlank()) { + header(BuildConfig.AUTHORIZATION_HEADER_NAME, BuildConfig.AUTHORIZATION_HEADER_VALUE) + } + } .build() chain.proceed(request) From e0f5096eb71c802ccacb3e04fbab3dd4fcf13872 Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 6 Mar 2024 17:58:08 +0100 Subject: [PATCH 3/8] Move shopper reference out of build config and add it as a default value to settings COAND-831 --- example-app/example.local.gradle | 1 - .../com/adyen/checkout/example/data/storage/KeyValueStorage.kt | 2 +- example-app/src/main/res/values/strings.xml | 1 + example-app/src/main/res/xml/preferences.xml | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example-app/example.local.gradle b/example-app/example.local.gradle index 84245de1c1..91799d1d88 100644 --- a/example-app/example.local.gradle +++ b/example-app/example.local.gradle @@ -12,7 +12,6 @@ android { buildConfigField "String", "MERCHANT_ACCOUNT", '"YOUR_MERCHANT_ACCOUNT"' buildConfigField "String", "AUTHORIZATION_HEADER_NAME", '"YOUR_AUTHORIZATION_HEADER_NAME"' buildConfigField "String", "AUTHORIZATION_HEADER_VALUE", '"YOUR_AUTHORIZATION_HEADER_VALUE"' - buildConfigField "String", "SHOPPER_REFERENCE", '"SHOPPER_REFERENCE"' } release { diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt b/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt index 1f02b61e6b..865c5f9034 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/storage/KeyValueStorage.kt @@ -48,7 +48,7 @@ internal class DefaultKeyValueStorage( return sharedPreferences.getString( appContext = appContext, stringRes = R.string.shopper_reference_key, - defaultValue = BuildConfig.SHOPPER_REFERENCE, + defaultStringRes = R.string.preferences_default_shopper_reference, ) } diff --git a/example-app/src/main/res/values/strings.xml b/example-app/src/main/res/values/strings.xml index b91afd257a..a3bfde5856 100644 --- a/example-app/src/main/res/values/strings.xml +++ b/example-app/src/main/res/values/strings.xml @@ -91,5 +91,6 @@ wechatpaySDK true ALL + test-android-components diff --git a/example-app/src/main/res/xml/preferences.xml b/example-app/src/main/res/xml/preferences.xml index bdf2edb8b4..af855edbc0 100644 --- a/example-app/src/main/res/xml/preferences.xml +++ b/example-app/src/main/res/xml/preferences.xml @@ -45,6 +45,7 @@ From 10b2c39794ac177e39d17e74a28a553743a0370d Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 6 Mar 2024 17:58:39 +0100 Subject: [PATCH 4/8] Fix wrong key for dark theme preference --- .../checkout/example/ui/configuration/ConfigurationActivity.kt | 2 +- example-app/src/main/res/xml/preferences.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt index 1605e39614..4a91375fcd 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt @@ -50,7 +50,7 @@ class ConfigurationActivity : AppCompatActivity() { super.onViewCreated(view, savedInstanceState) preferenceManager.preferenceScreen - .findPreference(requireContext().getString(R.string.night_theme_title)) + .findPreference(requireContext().getString(R.string.night_theme_key)) ?.setOnPreferenceChangeListener { _, newValue -> nightThemeRepository.theme = NightTheme.findByPreferenceValue(newValue as String?) true diff --git a/example-app/src/main/res/xml/preferences.xml b/example-app/src/main/res/xml/preferences.xml index af855edbc0..986c71e1b7 100644 --- a/example-app/src/main/res/xml/preferences.xml +++ b/example-app/src/main/res/xml/preferences.xml @@ -131,7 +131,7 @@ android:defaultValue="@string/night_theme_system" android:entries="@array/night_theme_entries" android:entryValues="@array/night_theme_values" - android:key="@string/night_theme_title" + android:key="@string/night_theme_key" android:title="@string/night_theme_title" app:useSimpleSummaryProvider="true" /> From f2e1ec58306f964a21f2a2e076a34ddbfc82ee48 Mon Sep 17 00:00:00 2001 From: josephj Date: Wed, 6 Mar 2024 19:13:12 +0100 Subject: [PATCH 5/8] Display default value of merchant account in settings screen COAND-831 --- .../ui/configuration/ConfigurationActivity.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt index 4a91375fcd..284e3b50b1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt @@ -12,8 +12,10 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.preference.DropDownPreference +import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat import com.adyen.checkout.example.R +import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.databinding.ActivitySettingsBinding import com.adyen.checkout.example.ui.theme.NightTheme import com.adyen.checkout.example.ui.theme.NightThemeRepository @@ -39,6 +41,9 @@ class ConfigurationActivity : AppCompatActivity() { @AndroidEntryPoint class ConfigurationFragment : PreferenceFragmentCompat() { + @Inject + lateinit var keyValueStorage: KeyValueStorage + @Inject internal lateinit var nightThemeRepository: NightThemeRepository @@ -49,12 +54,19 @@ class ConfigurationActivity : AppCompatActivity() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - preferenceManager.preferenceScreen + preferenceManager .findPreference(requireContext().getString(R.string.night_theme_key)) ?.setOnPreferenceChangeListener { _, newValue -> nightThemeRepository.theme = NightTheme.findByPreferenceValue(newValue as String?) true } + + /* This workaround is needed to display the default value of Merchant Account. We cannot set this value in + `preferences.xml` because it's only available in the code and there is no "clean" way to set the default + value programmatically. */ + preferenceManager + .findPreference(requireContext().getString(R.string.merchant_account_key)) + ?.text = keyValueStorage.getMerchantAccount() } } } From e75dd6b1f57d3f54eabca3b0753d3473f2612ae0 Mon Sep 17 00:00:00 2001 From: josephj Date: Thu, 7 Mar 2024 12:27:59 +0100 Subject: [PATCH 6/8] Redact authorization header from logs COAND-831 --- .../checkout/example/di/NetworkModule.kt | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt index 52e97c9df8..b0ad82c959 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt @@ -34,22 +34,25 @@ object NetworkModule { internal fun provideOkHttpClient(): OkHttpClient { val builder = OkHttpClient.Builder() + val authorizationHeader = (BuildConfig.AUTHORIZATION_HEADER_NAME to BuildConfig.AUTHORIZATION_HEADER_VALUE) + .takeIf { it.first.isNotBlank() } + if (BuildConfig.DEBUG) { - val interceptor = HttpLoggingInterceptor() - interceptor.level = HttpLoggingInterceptor.Level.BODY + val interceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + if (authorizationHeader != null) redactHeader(authorizationHeader.first) + } builder.addNetworkInterceptor(interceptor) } - builder.addInterceptor { chain -> - val request = chain.request().newBuilder() - .apply { - if (BuildConfig.AUTHORIZATION_HEADER_NAME.isNotBlank()) { - header(BuildConfig.AUTHORIZATION_HEADER_NAME, BuildConfig.AUTHORIZATION_HEADER_VALUE) - } - } - .build() + if (authorizationHeader != null) { + builder.addInterceptor { chain -> + val request = chain.request().newBuilder() + .header(authorizationHeader.first, authorizationHeader.second) + .build() - chain.proceed(request) + chain.proceed(request) + } } return builder.build() From a0fdfd89c4e72dbde694efee28f15f2cb9aec0df Mon Sep 17 00:00:00 2001 From: josephj Date: Fri, 8 Mar 2024 10:42:42 +0100 Subject: [PATCH 7/8] Use example file as it is build to allow building from CI COAND-831 --- example-app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example-app/build.gradle b/example-app/build.gradle index bcb9b6e49d..2538be2a78 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -18,6 +18,9 @@ apply from: "${rootDir}/config/gradle/ci.gradle" if (file("local.gradle").exists()) { apply from: "local.gradle" +} else if (System.getenv('CI')) { + // if building from CI use example file as it is to ensure the build passes + apply from: "example.local.gradle" } else { throw new GradleException("File example-app/local.gradle not found. Check example-app/README.md for more instructions.") } From 0b8aa4b8ae2f2de0b448eebf4de8592058f40b63 Mon Sep 17 00:00:00 2001 From: Oscar Spruit Date: Thu, 22 Feb 2024 12:48:10 +0100 Subject: [PATCH 8/8] Add section to README explaining how to run the example app COAND-831 --- README.md | 4 ++++ example-app/README.md | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 example-app/README.md diff --git a/README.md b/README.md index 647b5d1fb5..f511e024c7 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ If you are upgrading from 4.x.x to a current release, check out our [migration g If you use ProGuard or R8, you do not need to manually add any rules, as they are automatically embedded in the artifacts. Please let us know if you find any issues. +## Development + +For development and testing purposes the project is accompanied by a test app. See [here](example-app/README.md) how to set it up and run it. + ## Support If you have a feature request, or spotted a bug or a technical problem, [create an issue here][github.newIssue]. diff --git a/example-app/README.md b/example-app/README.md new file mode 100644 index 0000000000..345c63032f --- /dev/null +++ b/example-app/README.md @@ -0,0 +1,27 @@ +# Example app + +The `example-app` module is used for development and testing purposes. It should not be used as a template for your own integration. Check out the [docs](https://docs.adyen.com/online-payments/build-your-integration/) for best practices on integration. + +## Running the app + +Steps to run the example app: +1. Build a server that acts as a proxy between the app and the Adyen Checkout API. + * Your server should mirror the necessary endpoints for your flow (for example [/sessions](https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions) for the sessions flow and [/paymentMethods](https://docs.adyen.com/api-explorer/Checkout/latest/post/paymentMethods), [/payments](https://docs.adyen.com/api-explorer/Checkout/latest/post/payments) and [/payments/details](https://docs.adyen.com/api-explorer/Checkout/latest/post/payments/details) for the advanced flow). + * The API key should be managed on the server. +2. Duplicate `example.local.gradle` and name it `local.gradle`. Make sure the file is placed in the `example-app` directory. +3. Replace the predefined values: + * `MERCHANT_SERVER_URL`: the URL to your server. + * `CLIENT_KEY`: your client key. Find out how to obtain it [here](https://docs.adyen.com/development-resources/client-side-authentication/#get-your-client-key). + * `MERCHANT_ACCOUNT`: your merchant account identifier. + * `AUTHORIZATION_HEADER_NAME`: the name of the authorization header as expected by your server. You can use an empty string if this is not applicable for you. + * `AUTHORIZATION_HEADER_VALUE`: the value for the authorization header. You can use an empty string if this is not applicable for you. +4. Sync the project. +5. Run on any device or emulator. + +> [!WARNING] +> In case you don't have your own server you can connect to the Adyen Checkout API directly for testing purposes only. Be aware this could potentially leak your credentials, the market-ready application must never connect to Adyen API directly. + +To connect to the Adyen Checkout API directly you can use the following values: +* `MERCHANT_SERVER_URL`: `https://checkout-test.adyen.com/{VERSION}/` (check [here](https://docs.adyen.com/api-explorer/Checkout/latest/overview) for the latest version). +* `AUTHORIZATION_HEADER_NAME`: `x-api-key`. +* `AUTHORIZATION_HEADER_VALUE`: your API key.