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. diff --git a/example-app/build.gradle b/example-app/build.gradle index 6a523c3a96..2538be2a78 100644 --- a/example-app/build.gradle +++ b/example-app/build.gradle @@ -18,9 +18,11 @@ apply from: "${rootDir}/config/gradle/ci.gradle" if (file("local.gradle").exists()) { apply from: "local.gradle" +} else if (System.getenv('CI')) { + // if building from CI use example file as it is to ensure the build passes + apply from: "example.local.gradle" } else { - logger.lifecycle("File example-app/local.gradle not found. Falling back to default file with no values.") - apply from: "default.local.gradle" + throw new GradleException("File example-app/local.gradle not found. Check example-app/README.md for more instructions.") } // This runConnectedAndroidTest.gradle script is applied, diff --git a/example-app/default.local.gradle b/example-app/default.local.gradle deleted file mode 100644 index 34679f12c5..0000000000 --- a/example-app/default.local.gradle +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Replace the values in with the configuration to connect to YOUR Server. The <> should also be removed. - * Your server is the one that should connect to Adyen, DO NOT connect to Adyen directly from the App as this might expose your API keys. - * - * For a permanent local configuration file not tracked by Git, copy this file and rename it to "local.gradle" with your credentials. - */ -android { - buildTypes { - debug { - buildConfigField "String", "MERCHANT_ACCOUNT", "\"\"" - buildConfigField "String", "MERCHANT_SERVER_URL", "\"\"" - buildConfigField "String", "API_KEY_HEADER_NAME", "\"\"" - buildConfigField "String", "CHECKOUT_API_KEY", "\"\"" - buildConfigField "String", "CLIENT_KEY", "\"\"" - buildConfigField "String", "SHOPPER_REFERENCE", "\"\"" - } - - release { - initWith debug - matchingFallbacks = ['debug'] - } - } -} diff --git a/example-app/example.local.gradle b/example-app/example.local.gradle new file mode 100644 index 0000000000..91799d1d88 --- /dev/null +++ b/example-app/example.local.gradle @@ -0,0 +1,22 @@ +/** + * Duplicate this file into "local.gradle" then replace the placeholder values with the correct + * parameters. You might need to escape some characters if you see an error. + * + * DO NOT commit the new file anywhere public, you might be exposing your secret credentials. + */ +android { + buildTypes { + debug { + buildConfigField "String", "MERCHANT_SERVER_URL", '"YOUR_SERVER_URL"' + buildConfigField "String", "CLIENT_KEY", '"YOUR_CLIENT_KEY"' + buildConfigField "String", "MERCHANT_ACCOUNT", '"YOUR_MERCHANT_ACCOUNT"' + buildConfigField "String", "AUTHORIZATION_HEADER_NAME", '"YOUR_AUTHORIZATION_HEADER_NAME"' + buildConfigField "String", "AUTHORIZATION_HEADER_VALUE", '"YOUR_AUTHORIZATION_HEADER_VALUE"' + } + + release { + initWith debug + matchingFallbacks = ['debug'] + } + } +} diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt index 40fe6f11ff..8e694ba992 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/data/api/CheckoutApiService.kt @@ -9,7 +9,6 @@ package com.adyen.checkout.example.data.api import com.adyen.checkout.components.core.PaymentMethodsApiResponse -import com.adyen.checkout.example.BuildConfig import com.adyen.checkout.example.data.api.model.BalanceRequest import com.adyen.checkout.example.data.api.model.CancelOrderRequest import com.adyen.checkout.example.data.api.model.CreateOrderRequest @@ -27,14 +26,6 @@ import retrofit2.http.Query internal interface CheckoutApiService { - companion object { - private const val DEFAULT_GRADLE_SERVER_URL = "" - - fun isRealUrlAvailable(): Boolean { - return BuildConfig.MERCHANT_SERVER_URL != DEFAULT_GRADLE_SERVER_URL - } - } - @POST("sessions") suspend fun sessionsAsync(@Body sessionRequest: SessionRequest): SessionModel diff --git a/example-app/src/main/java/com/adyen/checkout/example/data/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/java/com/adyen/checkout/example/di/NetworkModule.kt b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt index e4ad77f02f..b0ad82c959 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/di/NetworkModule.kt @@ -29,29 +29,30 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object NetworkModule { - private val BASE_URL = if (CheckoutApiService.isRealUrlAvailable()) { - BuildConfig.MERCHANT_SERVER_URL - } else { - "http://myserver.com/my/endpoint/" - } - @Singleton @Provides internal fun provideOkHttpClient(): OkHttpClient { val builder = OkHttpClient.Builder() + val authorizationHeader = (BuildConfig.AUTHORIZATION_HEADER_NAME to BuildConfig.AUTHORIZATION_HEADER_VALUE) + .takeIf { it.first.isNotBlank() } + if (BuildConfig.DEBUG) { - val interceptor = HttpLoggingInterceptor() - interceptor.level = HttpLoggingInterceptor.Level.BODY + val interceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + if (authorizationHeader != null) redactHeader(authorizationHeader.first) + } builder.addNetworkInterceptor(interceptor) } - builder.addInterceptor { chain -> - val request = chain.request().newBuilder() - .addHeader(BuildConfig.API_KEY_HEADER_NAME, BuildConfig.CHECKOUT_API_KEY) - .build() + if (authorizationHeader != null) { + builder.addInterceptor { chain -> + val request = chain.request().newBuilder() + .header(authorizationHeader.first, authorizationHeader.second) + .build() - chain.proceed(request) + chain.proceed(request) + } } return builder.build() @@ -63,7 +64,7 @@ object NetworkModule { Moshi.Builder() .add(JSONObjectAdapter()) .add(KotlinJsonAdapterFactory()) - .build() + .build(), ) @Singleton @@ -74,7 +75,7 @@ object NetworkModule { converterFactory: Converter.Factory, ): Retrofit = Retrofit.Builder() - .baseUrl(BASE_URL) + .baseUrl(BuildConfig.MERCHANT_SERVER_URL) .client(okHttpClient) .addConverterFactory(converterFactory) .build() diff --git a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt index 1605e39614..284e3b50b1 100644 --- a/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt +++ b/example-app/src/main/java/com/adyen/checkout/example/ui/configuration/ConfigurationActivity.kt @@ -12,8 +12,10 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.preference.DropDownPreference +import androidx.preference.EditTextPreference import androidx.preference.PreferenceFragmentCompat import com.adyen.checkout.example.R +import com.adyen.checkout.example.data.storage.KeyValueStorage import com.adyen.checkout.example.databinding.ActivitySettingsBinding import com.adyen.checkout.example.ui.theme.NightTheme import com.adyen.checkout.example.ui.theme.NightThemeRepository @@ -39,6 +41,9 @@ class ConfigurationActivity : AppCompatActivity() { @AndroidEntryPoint class ConfigurationFragment : PreferenceFragmentCompat() { + @Inject + lateinit var keyValueStorage: KeyValueStorage + @Inject internal lateinit var nightThemeRepository: NightThemeRepository @@ -49,12 +54,19 @@ class ConfigurationActivity : AppCompatActivity() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - preferenceManager.preferenceScreen - .findPreference(requireContext().getString(R.string.night_theme_title)) + preferenceManager + .findPreference(requireContext().getString(R.string.night_theme_key)) ?.setOnPreferenceChangeListener { _, newValue -> nightThemeRepository.theme = NightTheme.findByPreferenceValue(newValue as String?) true } + + /* This workaround is needed to display the default value of Merchant Account. We cannot set this value in + `preferences.xml` because it's only available in the code and there is no "clean" way to set the default + value programmatically. */ + preferenceManager + .findPreference(requireContext().getString(R.string.merchant_account_key)) + ?.text = keyValueStorage.getMerchantAccount() } } } diff --git a/example-app/src/main/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..986c71e1b7 100644 --- a/example-app/src/main/res/xml/preferences.xml +++ b/example-app/src/main/res/xml/preferences.xml @@ -45,6 +45,7 @@ @@ -130,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" />