From 33616ee4565a847ca83a63ed04816208812cbf14 Mon Sep 17 00:00:00 2001 From: josephj Date: Mon, 6 May 2024 16:16:00 +0200 Subject: [PATCH 1/4] Extract launching custom tabs to a new class COAND-907 --- .../core/internal/DefaultRedirectHandler.kt | 22 +++-------- .../core/internal/util/CustomTabsLauncher.kt | 37 +++++++++++++++++++ .../ui/core/internal/util/PdfOpener.kt | 22 +++-------- .../internal/ui/view/FullVoucherView.kt | 22 +++-------- 4 files changed, 52 insertions(+), 51 deletions(-) create mode 100644 ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt index 8adeaafddb..ed18556010 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/DefaultRedirectHandler.kt @@ -14,13 +14,11 @@ import android.net.Uri import android.os.Build import androidx.annotation.RequiresApi import androidx.annotation.RestrictTo -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.exception.CheckoutException import com.adyen.checkout.core.exception.ComponentException import com.adyen.checkout.core.internal.util.adyenLog -import com.adyen.checkout.ui.core.internal.util.ThemeUtil +import com.adyen.checkout.ui.core.internal.util.CustomTabsLauncher import org.json.JSONException import org.json.JSONObject import java.lang.ref.WeakReference @@ -153,25 +151,15 @@ class DefaultRedirectHandler : RedirectHandler { private fun launchWithCustomTabs(context: Context, uri: Uri): Boolean { // open in custom tabs if there's no native app for the target uri - val defaultColors = CustomTabColorSchemeParams.Builder() - .setToolbarColor(ThemeUtil.getPrimaryThemeColor(context)) - .build() - - @Suppress("SwallowedException") - return try { - CustomTabsIntent.Builder() - .setShowTitle(true) - .setDefaultColorSchemeParams(defaultColors) - .build() - .launchUrl(context, uri) + val isLaunched = CustomTabsLauncher.launchCustomTab(context, uri) + if (isLaunched) { adyenLog(AdyenLogLevel.DEBUG) { "launchWithCustomTabs - redirect successful with custom tabs" } - true - } catch (e: ActivityNotFoundException) { + } else { adyenLog(AdyenLogLevel.DEBUG) { "launchWithCustomTabs - device doesn't support custom tabs or chrome is disabled" } - false } + return isLaunched } /** diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt new file mode 100644 index 0000000000..deadbb4fb4 --- /dev/null +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 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 6/5/2024. + */ + +package com.adyen.checkout.ui.core.internal.util + +import android.content.ActivityNotFoundException +import android.content.Context +import android.net.Uri +import androidx.annotation.RestrictTo +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent + +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +object CustomTabsLauncher { + fun launchCustomTab(context: Context, uri: Uri): Boolean { + val defaultColors = CustomTabColorSchemeParams.Builder() + .setToolbarColor(ThemeUtil.getPrimaryThemeColor(context)) + .build() + + @Suppress("SwallowedException") + return try { + CustomTabsIntent.Builder() + .setShowTitle(true) + .setDefaultColorSchemeParams(defaultColors) + .build() + .launchUrl(context, uri) + true + } catch (e: ActivityNotFoundException) { + false + } + } +} diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt index 9d7705fa28..92cb0c6f80 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt @@ -14,8 +14,6 @@ import android.content.Intent import android.net.Uri import android.os.Build import androidx.annotation.RestrictTo -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent import com.adyen.checkout.core.AdyenLogLevel import com.adyen.checkout.core.internal.util.adyenLog @@ -63,23 +61,13 @@ class PdfOpener { private fun openInCustomTab(context: Context, uri: Uri): Boolean { // open in custom tabs if there's no native app for the target uri - val defaultColors = CustomTabColorSchemeParams.Builder() - .setToolbarColor(ThemeUtil.getPrimaryThemeColor(context)) - .build() - - return try { - CustomTabsIntent.Builder() - .setShowTitle(true) - .setDefaultColorSchemeParams(defaultColors) - .build() - .launchUrl(context, uri) - + val isLaunched = CustomTabsLauncher.launchCustomTab(context, uri) + if (isLaunched) { adyenLog(AdyenLogLevel.DEBUG) { "Successfully opened pdf in custom tab" } - true - } catch (e: ActivityNotFoundException) { - adyenLog(AdyenLogLevel.DEBUG, e) { "Couldn't open pdf in custom tab" } - false + } else { + adyenLog(AdyenLogLevel.ERROR) { "Couldn't open pdf in custom tab" } } + return isLaunched } private fun openInBrowser(context: Context, uri: Uri): Boolean { diff --git a/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/view/FullVoucherView.kt b/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/view/FullVoucherView.kt index a0789baa25..5999f0a762 100644 --- a/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/view/FullVoucherView.kt +++ b/voucher/src/main/java/com/adyen/checkout/voucher/internal/ui/view/FullVoucherView.kt @@ -8,15 +8,12 @@ package com.adyen.checkout.voucher.internal.ui.view -import android.content.ActivityNotFoundException import android.content.Context import android.net.Uri import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import androidx.annotation.StringRes -import androidx.browser.customtabs.CustomTabColorSchemeParams -import androidx.browser.customtabs.CustomTabsIntent import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.doOnNextLayout import androidx.core.view.isVisible @@ -31,7 +28,7 @@ import com.adyen.checkout.core.internal.util.adyenLog import com.adyen.checkout.ui.core.internal.ui.ComponentView import com.adyen.checkout.ui.core.internal.ui.LogoSize import com.adyen.checkout.ui.core.internal.ui.loadLogo -import com.adyen.checkout.ui.core.internal.util.ThemeUtil +import com.adyen.checkout.ui.core.internal.util.CustomTabsLauncher import com.adyen.checkout.ui.core.internal.util.formatFullStringWithHyperLink import com.adyen.checkout.ui.core.internal.util.setLocalizedTextFromStyle import com.adyen.checkout.voucher.R @@ -206,20 +203,11 @@ internal class FullVoucherView @JvmOverloads constructor( } private fun onReadInstructionsClicked(url: String) { - val defaultColors = CustomTabColorSchemeParams.Builder() - .setToolbarColor(ThemeUtil.getPrimaryThemeColor(context)) - .build() - - try { - CustomTabsIntent.Builder() - .setShowTitle(true) - .setDefaultColorSchemeParams(defaultColors) - .build() - .launchUrl(context, Uri.parse(url)) - + val isLaunched = CustomTabsLauncher.launchCustomTab(context, Uri.parse(url)) + if (isLaunched) { adyenLog(AdyenLogLevel.DEBUG) { "Successfully opened instructions in custom tab" } - } catch (e: ActivityNotFoundException) { - adyenLog(AdyenLogLevel.DEBUG, e) { "Couldn't open instructions in custom tab" } + } else { + adyenLog(AdyenLogLevel.ERROR) { "Couldn't open instructions in custom tab" } } } From 609fc375a7247f77ff0db71f45af519be3e77e9f Mon Sep 17 00:00:00 2001 From: josephj Date: Mon, 6 May 2024 17:27:54 +0200 Subject: [PATCH 2/4] Add separate style for custom tabs COAND-907 --- .../core/internal/util/CustomTabsLauncher.kt | 42 ++++++++++++++++--- .../ui/core/internal/util/ThemeUtil.kt | 32 -------------- ui-core/src/main/res/values/attrs.xml | 16 ++++--- ui-core/src/main/res/values/styles.xml | 10 +++++ 4 files changed, 58 insertions(+), 42 deletions(-) delete mode 100644 ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ThemeUtil.kt diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt index deadbb4fb4..242760f4f8 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt @@ -10,23 +10,22 @@ package com.adyen.checkout.ui.core.internal.util import android.content.ActivityNotFoundException import android.content.Context +import android.content.res.TypedArray import android.net.Uri import androidx.annotation.RestrictTo +import androidx.annotation.StyleableRes import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import com.adyen.checkout.ui.core.R @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) object CustomTabsLauncher { fun launchCustomTab(context: Context, uri: Uri): Boolean { - val defaultColors = CustomTabColorSchemeParams.Builder() - .setToolbarColor(ThemeUtil.getPrimaryThemeColor(context)) - .build() - @Suppress("SwallowedException") return try { CustomTabsIntent.Builder() .setShowTitle(true) - .setDefaultColorSchemeParams(defaultColors) + .setDefaultColorSchemeParams(getDefaultColorSchemeParams(context)) .build() .launchUrl(context, uri) true @@ -34,4 +33,37 @@ object CustomTabsLauncher { false } } + + private fun getDefaultColorSchemeParams(context: Context): CustomTabColorSchemeParams { + val typedArray = context.obtainStyledAttributes( + R.style.AdyenCheckout_CustomTabs, + R.styleable.AdyenCheckoutCustomTabs, + ) + val toolbarColor = typedArray.getColorOrNull( + R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsToolbarColor, + ) + val secondaryToolbarColor = typedArray.getColorOrNull( + R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsSecondaryToolbarColor, + ) + val navigationBarColor = typedArray.getColorOrNull( + R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsNavigationBarColor, + ) + val navigationBarDividerColor = typedArray.getColorOrNull( + R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsNavigationBarDividerColor, + ) + typedArray.recycle() + + return CustomTabColorSchemeParams.Builder().apply { + toolbarColor?.let { setToolbarColor(it) } + secondaryToolbarColor?.let { setSecondaryToolbarColor(it) } + navigationBarColor?.let { setNavigationBarColor(it) } + navigationBarDividerColor?.let { setNavigationBarDividerColor(it) } + }.build() + } + + private fun TypedArray.getColorOrNull(@StyleableRes index: Int): Int? { + return getColor(index, COLOR_NOT_DEFINED).takeIf { it != COLOR_NOT_DEFINED } + } + + private const val COLOR_NOT_DEFINED = -1 } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ThemeUtil.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ThemeUtil.kt deleted file mode 100644 index 37cb8de9bc..0000000000 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/ThemeUtil.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2019 Adyen N.V. - * - * This file is open source and available under the MIT license. See the LICENSE file for more info. - * - * Created by caiof on 2/4/2019. - */ -package com.adyen.checkout.ui.core.internal.util - -import android.content.Context -import android.util.TypedValue -import androidx.annotation.AttrRes -import androidx.annotation.ColorInt -import androidx.annotation.RestrictTo - -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -object ThemeUtil { - - @ColorInt - fun getPrimaryThemeColor(context: Context): Int { - return getAttributeColor(context, androidx.appcompat.R.attr.colorPrimary) - } - - @ColorInt - private fun getAttributeColor(context: Context, @AttrRes attributeColor: Int): Int { - val typedValue = TypedValue() - val typedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(attributeColor)) - val color = typedArray.getColor(0, 0) - typedArray.recycle() - return color - } -} diff --git a/ui-core/src/main/res/values/attrs.xml b/ui-core/src/main/res/values/attrs.xml index 09d378992e..046d93efd2 100644 --- a/ui-core/src/main/res/values/attrs.xml +++ b/ui-core/src/main/res/values/attrs.xml @@ -1,5 +1,4 @@ - - + From 5d5b6af120670335c3b3f62bdd8db33fa2c381fb Mon Sep 17 00:00:00 2001 From: josephj Date: Mon, 6 May 2024 17:28:13 +0200 Subject: [PATCH 3/4] Update release notes and customization guide COAND-907 --- RELEASE_NOTES.md | 5 ++++- docs/UI_CUSTOMIZATION.md | 21 ++++++++++++++++++- .../core/internal/util/CustomTabsLauncher.kt | 4 +--- .../ui/core/internal/util/PdfOpener.kt | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3c0a0a62fd..aff3d3ff12 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,13 +1,16 @@ [//]: # (This file will be used for the release notes on GitHub when publishing.) [//]: # (Types of changes: `Breaking changes` `New` `Added` `Improved` `Changed` `Deprecated` `Removed` `Fixed`) [//]: # (Example:) -[//]: # (## Added) +[//]: # (## New) [//]: # ( - New payment method) [//]: # (## Changed) [//]: # ( - DropIn service's package changed from `com.adyen.dropin` to `com.adyen.dropin.services`) [//]: # (## Deprecated) [//]: # ( - Configurations public constructor are deprecated, please use each Configuration's builder to make a Configuration object) +## New +- For external redirects launched in a Custom Tab, you can now [customize the toolbar and navigation bar colors](docs/UI_CUSTOMIZATION.md#styling-custom-tabs) of the Custom Tab. + ## Fixed - Fixed various memory leaks. - Drop-in no longer overrides the log level in case of debug builds. diff --git a/docs/UI_CUSTOMIZATION.md b/docs/UI_CUSTOMIZATION.md index 18e620d8d9..3b35896062 100644 --- a/docs/UI_CUSTOMIZATION.md +++ b/docs/UI_CUSTOMIZATION.md @@ -10,6 +10,7 @@ The base theme can be extended and overridden. You can also use a custom base th - [Customizing a specific view](#customizing-a-specific-view) - [Adding dark mode support](#adding-dark-mode-support) - [Overriding string resources](#overriding-string-resources) +- [Styling Custom Tabs](#styling-custom-tabs) ## Customizing the base theme @@ -125,4 +126,22 @@ CheckoutConfiguration(shopperLocale, environment, clientKey) { } ``` -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). +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). + +## Styling Custom Tabs + +We use Custom Tabs to launch any external redirects that cannot be handled inside the SDK or by another installed native app. +By default we set the toolbar color to match the `colorPrimary` attribute defined in your theme. +To change this color to a different value than your `colorPrimary` attribute, you can override the `AdyenCheckout.CustomTabs` style in your `styles.xml`: + +```xml + +``` diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt index 242760f4f8..ad0c5062a1 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt @@ -62,8 +62,6 @@ object CustomTabsLauncher { } private fun TypedArray.getColorOrNull(@StyleableRes index: Int): Int? { - return getColor(index, COLOR_NOT_DEFINED).takeIf { it != COLOR_NOT_DEFINED } + return getColor(index, -1).takeIf { it != -1 } } - - private const val COLOR_NOT_DEFINED = -1 } diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt index 92cb0c6f80..091531e8d9 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/PdfOpener.kt @@ -65,7 +65,7 @@ class PdfOpener { if (isLaunched) { adyenLog(AdyenLogLevel.DEBUG) { "Successfully opened pdf in custom tab" } } else { - adyenLog(AdyenLogLevel.ERROR) { "Couldn't open pdf in custom tab" } + adyenLog(AdyenLogLevel.DEBUG) { "Couldn't open pdf in custom tab" } } return isLaunched } From 4b348a9e79741c122d1f4a06ca129987c24283f7 Mon Sep 17 00:00:00 2001 From: josephj Date: Tue, 14 May 2024 17:30:15 +0200 Subject: [PATCH 4/4] Directly use attr instead of inside a styleable COAND-907 --- .../core/internal/util/CustomTabsLauncher.kt | 31 ++++++------------- ui-core/src/main/res/values/attrs.xml | 10 +++--- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt index ad0c5062a1..6ebe7ed2b5 100644 --- a/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt +++ b/ui-core/src/main/java/com/adyen/checkout/ui/core/internal/util/CustomTabsLauncher.kt @@ -10,10 +10,9 @@ package com.adyen.checkout.ui.core.internal.util import android.content.ActivityNotFoundException import android.content.Context -import android.content.res.TypedArray import android.net.Uri +import androidx.annotation.AttrRes import androidx.annotation.RestrictTo -import androidx.annotation.StyleableRes import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import com.adyen.checkout.ui.core.R @@ -35,23 +34,10 @@ object CustomTabsLauncher { } private fun getDefaultColorSchemeParams(context: Context): CustomTabColorSchemeParams { - val typedArray = context.obtainStyledAttributes( - R.style.AdyenCheckout_CustomTabs, - R.styleable.AdyenCheckoutCustomTabs, - ) - val toolbarColor = typedArray.getColorOrNull( - R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsToolbarColor, - ) - val secondaryToolbarColor = typedArray.getColorOrNull( - R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsSecondaryToolbarColor, - ) - val navigationBarColor = typedArray.getColorOrNull( - R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsNavigationBarColor, - ) - val navigationBarDividerColor = typedArray.getColorOrNull( - R.styleable.AdyenCheckoutCustomTabs_adyenCustomTabsNavigationBarDividerColor, - ) - typedArray.recycle() + val toolbarColor = context.getColorOrNull(R.attr.adyenCustomTabsToolbarColor) + val secondaryToolbarColor = context.getColorOrNull(R.attr.adyenCustomTabsSecondaryToolbarColor) + val navigationBarColor = context.getColorOrNull(R.attr.adyenCustomTabsNavigationBarColor) + val navigationBarDividerColor = context.getColorOrNull(R.attr.adyenCustomTabsNavigationBarDividerColor) return CustomTabColorSchemeParams.Builder().apply { toolbarColor?.let { setToolbarColor(it) } @@ -61,7 +47,10 @@ object CustomTabsLauncher { }.build() } - private fun TypedArray.getColorOrNull(@StyleableRes index: Int): Int? { - return getColor(index, -1).takeIf { it != -1 } + private fun Context.getColorOrNull(@AttrRes attribute: Int): Int? { + val typedArray = obtainStyledAttributes(R.style.AdyenCheckout_CustomTabs, intArrayOf(attribute)) + val color = typedArray.getColor(0, -1).takeIf { it != -1 } + typedArray.recycle() + return color } } diff --git a/ui-core/src/main/res/values/attrs.xml b/ui-core/src/main/res/values/attrs.xml index 046d93efd2..aa2c582726 100644 --- a/ui-core/src/main/res/values/attrs.xml +++ b/ui-core/src/main/res/values/attrs.xml @@ -13,10 +13,8 @@ - - - - - - + + + +