Repository: mdn/kuma

Issue:
T - Subscription Banner
**Parent User Story:** #6554

**Description:**

The Subscription banner advertises requests for contributions to MDN

**Acceptance Criteria:**
- [x] banner shows on MDN Doc pages and homepage
- [x] banner only displays to logged in users (waffle)
- [x] banner only shows to subset of users (e.g. 50%)
- [x] banner only shows on desktop (not mobile)
- [x] banner is shown on top of the content at the bottom of the view port. (note: The newsletter CTA is shown at the bottom of the page. We might change the newsletter CTA to be a CTA for the monthly subscription, but that is independent of the banner.)
- [x] banner links to FAQ
- [x] banner links to edit profile page where user can sign up for subscription
- [x] banner has option for dismissal
- [x] dismissing banner keeps it quiet for 7 days (using local storage to track this is fine)

**Design**
https://app.abstract.com/projects/9decc480-70fc-11e9-9453-4f3fbffea7ce/branches/64547606-339e-494d-8d45-559aa506d80a/commits/6ae7cc2467cf36a609b9b2bc5189dabf9c5c4e7e/files/96E76C7B-2AF8-4473-A39E-E81890F91955/layers/E3B8DB66-C6A4-4BDC-A2FF-A80A02DAC2E8?collectionId=0750a16f-5979-4db7-9b1b-d4d676607dcd&collectionLayerId=2536d7fc-cd8e-4b9b-9580-615dc1f76e70&mode=design

**NOTE**
- ideally build this simply first so that the dependent work can get started (pre-Stripe modal)


Discussion:
As discussed though, I will break this down further into two pieces:

1. Bare bones(1 point) to unblock Gregor
2. Colour in the rest(3 points)
@tobinmori After having seen @atopal's comment on Slack[https://mozilla.slack.com/archives/CLTBV3W9X/p1582582865013200], I see the work on the banner for this sprint as follows:

**Acceptance Criteria:**

1. [ ] banner shows on MDN Doc pages only (no need for profile or homepage)
2. [ ] banner only displays to logged in users (waffle)
3. [ ] banner only shows to a subset of users (e.g. 50%)
4. [ ] banner only shows on tablet and desktop (not mobile)
5. [ ] banner is shown on top of the content at the bottom of the viewport.
6. [ ] banner links to FAQ
7. [ ] banner links to **edit profile page where user can sign up for subscription**
8. [ ] banner has an option for dismissal
9. [ ] dismissing banner keeps it quiet for 7 days (using local storage to track this is fine)

Note item 7 above. This means that the issue assigned to @Gregoor will not be required for this sprint.

We will also not yet require the subscription header on the FAQ page, nor would we need the dedicated thank you page. Let me know if I misunderstood this. Thanks
Thanks @schalkneethling - I'm wondering if @gregoor could start on the 'Becoming' modal even though there's no trigger in place. . .or would that make things too confusing if he gets a head start?
> Thanks @schalkneethling - I'm wondering if @Gregoor could start on the 'Becoming' modal even though there's no trigger in place. . .or would that make things too confusing if he gets a head start?

He could, I just want to be careful that he does not end up being blocked by me. If there are more immediate/important work he can focus on first, that would probably be the better option in light of the scope change.
@malqinneh We have a defined layout format for these types of banners. See below:

![Screenshot 2020-03-02 at 21 11 54](https://user-images.githubusercontent.com/10350960/75709228-f0b89800-5cca-11ea-9ef5-cdf7ae0b2d2c.png)

It is very close to what is in the Sketch design file though. I wonder if we can tweak it slightly as below. It makes the overall implementation and maintenance much easier if we keep things consistent like this. Thank you

![Screenshot 2020-03-02 at 21 11 38](https://user-images.githubusercontent.com/10350960/75709289-0e85fd00-5ccb-11ea-9c21-efa32c0f95a3.png)

If we had a style guide then this would not have come as a surprise :)
The proposed solution /or re-purposing MDN survey banner does not work well. Can you please let us know whether we can continue with the original design spec as proposed in Abstract?
> The proposed solution /or re-purposing MDN survey banner does not work well. Can you please let us know whether we can continue with the original design spec as proposed in Abstract?

Thank you for the feedback @malqinneh. We surely can, this is the design you are referring to correct?

![Screenshot 2020-03-02 at 22 31 54](https://user-images.githubusercontent.com/10350960/75715248-c6b8a300-5cd5-11ea-89bd-290989c2b8a9.png)

One question is around responsiveness. Having all the content in a single line will quickly cause overflow on narrower desktop displays and tablets. Do we have a design spec for smaller desktop screens and tablets?
Note, the icon inside the `subscribe` button and the close icon is missing. There are two missing fonts when I open the design in Sketch.
We didn't prioritize the banner to be responsiveness at this point. In cases where users have narrower desktop displays or browsers, I'm OK with exploring keeping the title (Become a monthly supporter) and showing the two call-to-actions (Learn more and Subscribe). Basically, omit "Support MDN with a $5 monthly subscription." @atopal does this sound OK to you?
> "Support MDN with a $5 monthly subscription."

Is this not rather key information to provide upfront? 

While a large portion of our users does view the site at 1920x1080, our second-largest audience uses a screen resolution of 1366x768. This means that those people will not see the "Support MDN with a $5 monthly subscription." copy and will have to click on `Learn more` to get this information.

This means navigating away from their current page to a page that will not have the banner. Should the user decide to navigate back to the previous page and click "Subscribe", they will, in this iteration, be sent to the "Edit Profile" page where they will have to click "Subscribe" again.
Perhaps we can have everything in a single line on large desktop screens and then on standard screens and tablets we can slide the copy under the main title as in the second screenshot in https://github.com/mdn/kuma/issues/6577#issuecomment-593569852
The copy from title [Become a monthly supporter] to CTA [Subscribe button] spans 1100 px. How will users with 1366 px resolution not be able to see "Support MDN with a $5 monthly subscription"?
> Note, the icon inside the `subscribe` button and the close icon is missing. There are two missing fonts when I open the design in Sketch.

Let's omit the icon inside the button. Also, is there not a default close button that MDN uses elsewhere?
> Let's omit the icon inside the button. Also, is there not a default close button that MDN uses elsewhere?

Yes, it is used on the current developer needs banner. The SVG file is located here:
https://github.com/mdn/kuma/blob/master/jinja2/includes/icons/general/close.svg

> The copy from title [Become a monthly supporter] to CTA [Subscribe button] spans 1100 px. How will users with 1366 px resolution not be able to see "Support MDN with a $5 monthly subscription"?

Ah ok, if that is the case we should be ok. I will implement this tomorrow and we can then take it from there. Thank you for all the feedback @malqinneh 
> Yes, it is used on the current developer needs banner. The SVG file is located here:
> https://github.com/mdn/kuma/blob/master/jinja2/includes/icons/general/close.svg

Perfect. Let's go with that 👍 

> Ah ok, if that is the case we should be ok. I will implement this tomorrow and we can then take it from there. Thank you for all the feedback @malqinneh

Sounds good! Happy to help.

> We didn't prioritize the banner to be responsiveness at this point. In cases where users have narrower desktop displays or browsers, I'm OK with exploring keeping the title (Become a monthly supporter) and showing the two call-to-actions (Learn more and Subscribe). Basically, omit "Support MDN with a $5 monthly subscription." @atopal does this sound OK to you?

Yeah, okay with me below 1100px.
> banner links to payment 'Become a subscriber' modal (pre-Stripe modal)

The above acceptance criteria is incorrect. For this sprint, we are linking to `Edit Profile` instead
> > banner links to payment 'Become a subscriber' modal (pre-Stripe modal)
> 
> The above acceptance criteria is incorrect. For this sprint, we are linking to `Edit Profile` instead

Updated to be accurate!
> banner shows on MDN Doc pages only (no need for profile or homepage)

@atopal Is it a deal breaker if this banner **is** shown on the homepage? It will still only be for logged in users.

Resolving patch:
diff --git a/kuma/core/migrations/0004_create_subscription_banner_flag.py b/kuma/core/migrations/0004_create_subscription_banner_flag.py
new file mode 100644
--- /dev/null
+++ b/kuma/core/migrations/0004_create_subscription_banner_flag.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def create_subscription_banner_flag(apps, schema_editor):
+    Flag = apps.get_model("waffle", "Flag")
+    if not Flag.objects.filter(name="subscription_banner").exists():
+        Flag.objects.create(
+            name="subscription_banner", staff=True, note="Shows the subscription banner"
+        )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("core", "0003_auto_20191212_0638"),
+        # This is needed otherwise `apps.get_model('waffle', 'Flag')`
+        # will raise a Django app LookupError.
+        ("waffle", "0001_initial"),
+    ]
+
+    operations = [migrations.RunPython(create_subscription_banner_flag)]
diff --git a/kuma/settings/common.py b/kuma/settings/common.py
--- a/kuma/settings/common.py
+++ b/kuma/settings/common.py
@@ -672,6 +672,14 @@ def pipeline_one_scss(slug, **kwargs):
         "source_filenames": ("styles/components/banners/base.scss",),
         "output_filename": "build/styles/banners.css",
     },
+    "banner_developer_needs": {
+        "source_filenames": ("styles/components/banners/developer-needs.scss",),
+        "output_filename": "build/styles/developer-needs.css",
+    },
+    "banner_mdn_subscriptions": {
+        "source_filenames": ("styles/components/banners/mdn-subscriptions.scss",),
+        "output_filename": "build/styles/mdn-subscriptions.css",
+    },
     "home": {
         "source_filenames": ("styles/home.scss",),
         "output_filename": "build/styles/home.css",


Test patch:
diff --git a/kuma/api/v1/tests/test_views.py b/kuma/api/v1/tests/test_views.py
--- a/kuma/api/v1/tests/test_views.py
+++ b/kuma/api/v1/tests/test_views.py
@@ -216,6 +216,7 @@ def test_whoami_anonymous(client, settings, timezone):
                 "flag_all": True,
                 "flag_none": False,
                 "subscription": False,
+                "subscription_banner": False,
             },
             "switches": {"switch_on": True, "switch_off": False},
             "samples": {"sample_always": True, "sample_never": False},
@@ -275,6 +276,7 @@ def test_whoami(
                 "flag_all": True,
                 "flag_none": False,
                 "subscription": is_staff,
+                "subscription_banner": is_staff,
             },
             "switches": {"switch_on": True, "switch_off": False},
             "samples": {"sample_always": True, "sample_never": False},
diff --git a/kuma/javascript/src/banners.test.js b/kuma/javascript/src/banners.test.js
--- a/kuma/javascript/src/banners.test.js
+++ b/kuma/javascript/src/banners.test.js
@@ -8,9 +8,23 @@ import type { BannerProps } from './banners.jsx';
 
 const mockUserData = { ...UserProvider.defaultUserData };
 
+const authRequiredBanner: Array<BannerProps> = [
+    {
+        id: 'auth-flag',
+        classname: 'auth-class',
+        title: 'auth-title',
+        copy: 'auth-copy',
+        cta: 'auth-cta',
+        url: 'auth-link',
+        embargoDays: 7,
+        authenticated: true
+    }
+];
+
 const banners: Array<BannerProps> = [
     {
         id: 'flag1',
+        classname: 'class1',
         title: 'title1',
         copy: 'copy1',
         cta: 'cta1',
@@ -19,6 +33,7 @@ const banners: Array<BannerProps> = [
     },
     {
         id: 'flag2',
+        classname: 'class2',
         title: 'title2',
         copy: 'copy2',
         cta: 'cta2',
@@ -27,6 +42,19 @@ const banners: Array<BannerProps> = [
     }
 ];
 
+const opensInNewWindowBanner: Array<BannerProps> = [
+    {
+        id: 'newwin-flag',
+        classname: 'newwin-class',
+        title: 'newwin-title',
+        copy: 'newwin-copy',
+        cta: 'newwin-cta',
+        url: 'newwin-link',
+        embargoDays: 7,
+        newWindow: true
+    }
+];
+
 describe('Banners', () => {
     test('renders nothing if no waffle flags set', () => {
         let banner = create(
@@ -48,6 +76,7 @@ describe('Banners', () => {
             );
 
             let snapshot = JSON.stringify(banner.toJSON());
+            expect(snapshot).toContain(b.classname);
             expect(snapshot).toContain(b.title);
             expect(snapshot).toContain(b.copy);
             expect(snapshot).toContain(b.cta);
@@ -55,6 +84,44 @@ describe('Banners', () => {
         }
     });
 
+    test('renders banner for logged in users', () => {
+        for (let b of authRequiredBanner) {
+            mockUserData.isAuthenticated = true;
+            mockUserData.waffle.flags = {
+                [b.id]: true
+            };
+
+            let banner = create(
+                <UserProvider.context.Provider value={mockUserData}>
+                    <Banners banners={authRequiredBanner} />
+                </UserProvider.context.Provider>
+            );
+
+            let snapshot = JSON.stringify(banner.toJSON());
+            expect(snapshot).toContain(b.title);
+            expect(snapshot).toContain(b.copy);
+            expect(snapshot).toContain(b.cta);
+            expect(snapshot).toContain(b.url);
+        }
+    });
+
+    test('renders nothing if user not logged in', () => {
+        for (let b of authRequiredBanner) {
+            mockUserData.isAuthenticated = false;
+            mockUserData.waffle.flags = {
+                [b.id]: true
+            };
+
+            let banner = create(
+                <UserProvider.context.Provider value={mockUserData}>
+                    <Banners banners={authRequiredBanner} />
+                </UserProvider.context.Provider>
+            );
+
+            expect(banner.toJSON()).toBe(null);
+        }
+    });
+
     test('renders first banner and not second when both flags are set', () => {
         mockUserData.waffle.flags = {
             [banners[0].id]: true,
@@ -250,4 +317,54 @@ describe('Banners', () => {
 
         expect(banner.toJSON()).toBe(null);
     });
+
+    test('adds target and rel for banners set to open in new window', () => {
+        for (let b of opensInNewWindowBanner) {
+            mockUserData.isAuthenticated = false;
+            mockUserData.waffle.flags = {
+                [b.id]: true
+            };
+
+            let banner = create(
+                <UserProvider.context.Provider value={mockUserData}>
+                    <Banners banners={opensInNewWindowBanner} />
+                </UserProvider.context.Provider>
+            );
+
+            let ctaButtonProps = banner.root.findByType('a').props;
+
+            let snapshot = JSON.stringify(banner.toJSON());
+            expect(snapshot).toContain(b.title);
+            expect(snapshot).toContain(b.copy);
+            expect(snapshot).toContain(b.cta);
+            expect(snapshot).toContain(b.url);
+            expect(ctaButtonProps.target).toBeDefined();
+            expect(ctaButtonProps.rel).toBeDefined();
+        }
+    });
+
+    test('does not add target and rel for default banners', () => {
+        for (let b of authRequiredBanner) {
+            mockUserData.isAuthenticated = true;
+            mockUserData.waffle.flags = {
+                [b.id]: true
+            };
+
+            let banner = create(
+                <UserProvider.context.Provider value={mockUserData}>
+                    <Banners banners={authRequiredBanner} />
+                </UserProvider.context.Provider>
+            );
+
+            let ctaButtonProps = banner.root.findByType('a').props;
+
+            let snapshot = JSON.stringify(banner.toJSON());
+            expect(snapshot).toContain(b.title);
+            expect(snapshot).toContain(b.copy);
+            expect(snapshot).toContain(b.cta);
+            expect(snapshot).toContain(b.url);
+            expect(ctaButtonProps.target).not.toBeDefined();
+            expect(ctaButtonProps.rel).not.toBeDefined();
+        }
+    });
 });