Repository: safe-global/safe-config-service

Issue:
Add theme information to the chain endpoint
Frontend clients have a theming option for each L2 chain. This information should be stored and retrieved with `/api/v1/chains` so that clients can render the respective theme for each chain.

The following should be added to the `/api/v1/chains` endpoint.

### Initial proposal:

```javascript
{
  ...
  "theme": {
       "light": {
           "textColor": <hex-color-string>,
           "backgroundColor": <hex-color-string>
       },
       "dark": {
           "textColor": <hex-color-string>,
           "backgroundColor": <hex-color-string>
       }
   }
}
```

### Update on the payload: https://github.com/gnosis/safe-config-service/issues/86#issuecomment-862340184


Discussion:
The text <> background color differentiation make sense since some require a black-ish text, other white-ish.
The colors are selected depending on the branding of the respective network.  Hence I'm not so sure whether we really need to differentiate between light and dark theme. What I mean is e.g. Optimism will have this red color either way (https://optimism.io/), no matter if in light or dark color.

cc @elgatovital did you think about this already by any chance?
>  Hence I'm not so sure whether we really need to differentiate between light and dark theme. What I mean is e.g. Optimism will have this red color either way (https://optimism.io/), no matter if in light or dark color.

So this means that for the clients that support light/dark mode these network colours should not change correct?
> So this means that for the clients that support light/dark mode these network colours should not change correct?

Yes, in my opinion there is no point in using different colors for light vs. dark currently. Perhaps later.

I think though that each network should have a background color and a separate text color. That's how we design it currently on mobile and web. On some colors, a bright text color is needed. On other a darker one. 
> > So this means that for the clients that support light/dark mode these network colours should not change correct?
> 
> Yes, in my opinion there is no point in using different colors for light vs. dark currently. Perhaps later.
> 
> I think though that each network should have a background color and a separate text color. That's how we design it currently on mobile and web. On some colors, a bright text color is needed. On other a darker one.

I see. So following your suggestion I would simplify it further while leaving the text color and background color available as separate fields:

```javascript
"theme": {
    "textColor": <hex-color-string>,
    "backgroundColor": <hex-color-string>
}
```
Maybe it would be better to return an array rather than an object (so we leave the door open to the possibility of having many themes?
> Maybe it would be better to return an array rather than an object (so we leave the door open to the possibility of having many themes?

I'd be conservative in the beginning and leave it as a single object for the `theme` as this is what is expected on every client. Additionally if we have the distinction in the future between dark/light modes it might be easier on the client side to address these themes directly like `theme.light` and `theme.dark` instead of `themes[0]` and `themes[1]`
yeah, I think we would still need an additional field in that case to identify the individual theme. I think by index it would not be very effective, like you mentioned.

Resolving patch:
diff --git a/src/chains/migrations/0003_auto_20210617_1444.py b/src/chains/migrations/0003_auto_20210617_1444.py
new file mode 100644
--- /dev/null
+++ b/src/chains/migrations/0003_auto_20210617_1444.py
@@ -0,0 +1,46 @@
+# Generated by Django 3.2.4 on 2021-06-17 14:44
+
+import re
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("chains", "0002_chain_transaction_service_url"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="chain",
+            name="theme_background_color",
+            field=models.CharField(
+                default="#000",
+                max_length=9,
+                validators=[
+                    django.core.validators.RegexValidator(
+                        re.compile("^#(?:[0-9a-fA-F]{3,4}){1,2}$"),
+                        "Invalid hex color",
+                        "invalid",
+                    )
+                ],
+            ),
+        ),
+        migrations.AddField(
+            model_name="chain",
+            name="theme_text_color",
+            field=models.CharField(
+                default="#fff",
+                max_length=9,
+                validators=[
+                    django.core.validators.RegexValidator(
+                        re.compile("^#(?:[0-9a-fA-F]{3,4}){1,2}$"),
+                        "Invalid hex color",
+                        "invalid",
+                    )
+                ],
+            ),
+        ),
+    ]
diff --git a/src/chains/models.py b/src/chains/models.py
--- a/src/chains/models.py
+++ b/src/chains/models.py
@@ -1,5 +1,12 @@
+import re
+
+from django.core.validators import RegexValidator
 from django.db import models
 
+HEX_ARGB_REGEX = re.compile("^#(?:[0-9a-fA-F]{3,4}){1,2}$")
+
+color_validator = RegexValidator(HEX_ARGB_REGEX, "Invalid hex color", "invalid")
+
 
 class Chain(models.Model):
     id = models.PositiveBigIntegerField(verbose_name="Chain Id", primary_key=True)
@@ -10,6 +17,12 @@ class Chain(models.Model):
     currency_symbol = models.CharField(max_length=255)
     currency_decimals = models.IntegerField(default=18)
     transaction_service_url = models.URLField(null=True)
+    theme_text_color = models.CharField(
+        validators=[color_validator], max_length=9, default="#fff"
+    )
+    theme_background_color = models.CharField(
+        validators=[color_validator], max_length=9, default="#000"
+    )
 
     def __str__(self):
         return f"{self.name} | chain_id={self.id}"
diff --git a/src/chains/serializers.py b/src/chains/serializers.py
--- a/src/chains/serializers.py
+++ b/src/chains/serializers.py
@@ -4,6 +4,11 @@
 from .models import Chain
 
 
+class ThemeSerializer(serializers.Serializer):
+    text_color = serializers.CharField(source="theme_text_color")
+    background_color = serializers.CharField(source="theme_background_color")
+
+
 class CurrencySerializer(serializers.Serializer):
     name = serializers.CharField(source="currency_name")
     symbol = serializers.CharField(source="currency_symbol")
@@ -17,6 +22,7 @@ class ChainSerializer(serializers.ModelSerializer):
     transaction_service = serializers.URLField(
         source="transaction_service_url", default=None
     )
+    theme = serializers.SerializerMethodField()
 
     class Meta:
         model = Chain
@@ -27,9 +33,15 @@ class Meta:
             "block_explorer_url",
             "native_currency",
             "transaction_service",
+            "theme",
         ]
 
     @staticmethod
     @swagger_serializer_method(serializer_or_field=CurrencySerializer)
     def get_native_currency(obj):
         return CurrencySerializer(obj).data
+
+    @staticmethod
+    @swagger_serializer_method(serializer_or_field=ThemeSerializer)
+    def get_theme(obj):
+        return ThemeSerializer(obj).data


Test patch:
diff --git a/src/chains/tests/factories.py b/src/chains/tests/factories.py
--- a/src/chains/tests/factories.py
+++ b/src/chains/tests/factories.py
@@ -16,3 +16,5 @@ class Meta:
     currency_symbol = factory.Faker("cryptocurrency_code")
     currency_decimals = factory.Faker("pyint")
     transaction_service_url = factory.Faker("url")
+    theme_text_color = factory.Faker("hex_color")
+    theme_background_color = factory.Faker("hex_color")
diff --git a/src/chains/tests/test_models.py b/src/chains/tests/test_models.py
--- a/src/chains/tests/test_models.py
+++ b/src/chains/tests/test_models.py
@@ -1,4 +1,7 @@
-from django.test import TestCase
+from django.core.exceptions import ValidationError
+from django.db import DataError
+from django.test import TestCase, TransactionTestCase
+from faker import Faker
 
 from .factories import ChainFactory
 
@@ -10,3 +13,28 @@ def test_str_method_outputs_name_chain_id(self):
             str(chain),
             f"{chain.name} | chain_id={chain.id}",
         )
+
+
+class ChainColorValidationTestCase(TransactionTestCase):
+    def test_invalid_text_colors(self):
+        param_list = ["aaa", "bbb", "#fffffffff", "zzz", "010", "", "a word", "#hhh"]
+        for invalid_color in param_list:
+            with self.subTest(msg=f"Invalid color {invalid_color} should throw"):
+                with self.assertRaises(
+                    (
+                        ValidationError,
+                        DataError,
+                    )
+                ):
+                    chain = ChainFactory.create(theme_text_color=invalid_color)
+                    # run validators
+                    chain.full_clean()
+
+    def test_valid_text_colors(self):
+        param_list = ["#000", "#fff", "#00000000", "#ffffffff"] + [
+            Faker().hex_color() for _ in range(20)
+        ]
+        for valid_color in param_list:
+            with self.subTest(msg=f"Valid color {valid_color} should not throw"):
+                chain = ChainFactory.create(theme_text_color=valid_color)
+                chain.full_clean()
diff --git a/src/chains/tests/test_views.py b/src/chains/tests/test_views.py
--- a/src/chains/tests/test_views.py
+++ b/src/chains/tests/test_views.py
@@ -34,6 +34,10 @@ def test_json_payload_format(self):
                         "decimals": chain.currency_decimals,
                     },
                     "transactionService": chain.transaction_service_url,
+                    "theme": {
+                        "textColor": chain.theme_text_color,
+                        "backgroundColor": chain.theme_background_color,
+                    },
                 }
             ],
         }
@@ -114,6 +118,10 @@ def test_json_payload_format(self):
                 "decimals": chain.currency_decimals,
             },
             "transactionService": chain.transaction_service_url,
+            "theme": {
+                "textColor": chain.theme_text_color,
+                "backgroundColor": chain.theme_background_color,
+            },
         }
 
         response = self.client.get(path=url, data=None, format="json")
@@ -143,6 +151,10 @@ def test_match(self):
                 "decimals": chain.currency_decimals,
             },
             "transaction_service": chain.transaction_service_url,
+            "theme": {
+                "text_color": chain.theme_text_color,
+                "background_color": chain.theme_background_color,
+            },
         }
 
         response = self.client.get(path=url, data=None, format="json")