diff --git a/.ds.baseline b/.ds.baseline index 36b7821a7..0aff1ada1 100644 --- a/.ds.baseline +++ b/.ds.baseline @@ -161,7 +161,7 @@ "filename": "app/config.py", "hashed_secret": "577a4c667e4af8682ca431857214b3a920883efc", "is_verified": false, - "line_number": 118, + "line_number": 121, "is_secret": false } ], @@ -425,7 +425,7 @@ "filename": "deploy-config/sandbox.yml", "hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54", "is_verified": false, - "line_number": 8, + "line_number": 9, "is_secret": false } ], @@ -684,5 +684,5 @@ } ] }, - "generated_at": "2024-10-07T20:44:40Z" + "generated_at": "2024-10-29T19:28:03Z" } diff --git a/.gitignore b/.gitignore index aaa99d325..59b996de8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ *.XLSX ## Non user files allowed to be commited +!app/assets/pdf/tcpa_overview.pdf !tests/test_pdf_files/no_eof_marker.pdf !tests/test_pdf_files/multi_page_pdf.pdf !tests/test_pdf_files/big.pdf diff --git a/app/assets/images/best-practices-card-sprite.svg b/app/assets/images/best-practices-card-sprite.svg new file mode 100644 index 000000000..0591b73fb --- /dev/null +++ b/app/assets/images/best-practices-card-sprite.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/call-center.svg b/app/assets/images/call-center.svg new file mode 100644 index 000000000..f6e78b53a --- /dev/null +++ b/app/assets/images/call-center.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/community-org.svg b/app/assets/images/community-org.svg new file mode 100644 index 000000000..f03a16146 --- /dev/null +++ b/app/assets/images/community-org.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/grouping.svg b/app/assets/images/grouping.svg new file mode 100644 index 000000000..b79d7a18f --- /dev/null +++ b/app/assets/images/grouping.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/phone_grey.svg b/app/assets/images/phone_grey.svg new file mode 100644 index 000000000..0ef4f9541 --- /dev/null +++ b/app/assets/images/phone_grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/project-management.svg b/app/assets/images/project-management.svg new file mode 100644 index 000000000..f88a96e66 --- /dev/null +++ b/app/assets/images/project-management.svg @@ -0,0 +1 @@ + diff --git a/app/assets/images/snap-renewal.jpg b/app/assets/images/snap-renewal.jpg new file mode 100644 index 000000000..bd09b6cd1 Binary files /dev/null and b/app/assets/images/snap-renewal.jpg differ diff --git a/app/assets/images/website.svg b/app/assets/images/website.svg new file mode 100644 index 000000000..1ca731f45 --- /dev/null +++ b/app/assets/images/website.svg @@ -0,0 +1 @@ + diff --git a/app/assets/pdf/tcpa_overview.pdf b/app/assets/pdf/tcpa_overview.pdf new file mode 100644 index 000000000..61bf4a835 Binary files /dev/null and b/app/assets/pdf/tcpa_overview.pdf differ diff --git a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss index 61cd543ef..fe98aed09 100644 --- a/app/assets/sass/uswds/_uswds-theme-custom-styles.scss +++ b/app/assets/sass/uswds/_uswds-theme-custom-styles.scss @@ -671,3 +671,229 @@ details form { border: 1px solid #face00; border-radius: 7px; } + +.usa-prose.margin-top-0 { + margin-top: 0; +} + +ol.best-practices-list { + counter-reset: item; + list-style-type: none; + padding-left: 0; +} + +ol.best-practices-list.set-two { + counter-reset: item 6; +} + +ol.best-practices-list.set-three { + counter-reset: item 10; +} + +ol.best-practices-list li { + counter-increment: item; + margin-bottom: 15px; + position: relative; + padding-left: 40px; +} + +ol.best-practices-list li::before { + content: counter(item); + background-color: #005ea2; + color: white; + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + justify-content: center; + align-items: center; + line-height: 1; + position: absolute; + left: 0; + top: 0; +} + +li.best-practices { + padding-bottom: 50px; +} + +div.best-practices { + height: 400px +} + +$do-dont-color-do: "green-cool-50v"; +$do-dont-color-dont: "red-cool-50v"; +$do-dont-color-border: "gray-cool-20"; +$do-dont-padding: 3; +$do-dont-top-bar-width: 1; + +.do-dont { + display: flex; + height: 100%; + padding-top: units(2); +} + +.do-dont__do, +.do-dont__dont { + background-color: color("white"); + border: 1px solid color($do-dont-color-border); + flex: 1 0 0; + height: 100%; + position: relative; +} + +.do-dont__do { + padding: 0rem; +} + +.do-dont__do:before, +.do-dont__dont:before { + content: ""; + position: absolute; + height: units($do-dont-top-bar-width); + width: calc(100% + 2px); + top: -1px; + left: -1px; +} + +.do-dont__do:before { + background-color: color($do-dont-color-do); +} + +.do-dont__dont:before { + background-color: color($do-dont-color-dont); +} + +.do-dont__heading { + // @include typeset("lang", "lg", 2); + align-items: center; + border-bottom: 1px solid color($do-dont-color-border); + display: flex; + font-weight: fw("bold"); + margin: 0; + padding: units(4) units($do-dont-padding) units($do-dont-padding); + + .usa-icon { + @include u-square(3); + margin-right: units(0.5); + } +} + +.do-dont__do .do-dont__heading { + color: color($do-dont-color-do); +} + +.do-dont__dont .do-dont__heading { + color: color($do-dont-color-dont); +} + +.do-dont__content { + padding: 0 units($do-dont-padding) units($do-dont-padding) units($do-dont-padding); + + ul { + margin-bottom: 0; + overflow-wrap: anywhere; + } + + li+li { + margin-top: units(1); + } +} + +.site-note { + background-color: #dfe1e2; + max-width: 72ex; + padding: .5rem; + border-radius: 2px; +} + +.svg-circle-icon { + display: inline-block; + margin: 0; + padding: 0; + vertical-align: text-bottom; + margin-right: -2px; +} + +.height-phone-lg { + max-height: 500px; +} + +@media (max-width: 758px) { + .best-practices-flex-container { + flex-direction: column; + } +} + +.linked-content:hover { + cursor: pointer; + transform: scale(1.05); + transition: transform 0.3s ease, background-color 0.3s ease, +} + +li.linked-card:hover > div:first-child:hover { + border-color: #005ea2; +} + +li.linked-card:hover h4, +li.linked-card:hover p, +li.linked-card:hover svg, +.best-practices_card_img:hover{ + color: #005ea2; +} + +.best-practices_card_img { + display: block; + width: 50%; + height: auto; + margin: 0 auto; +} + +.best-practices_link { + text-decoration: none; +} + +.usa-link--downloadable { + position: relative; +} + +.usa-link--downloadable::after { + background-image: url(../img/material-icons/download.svg); + background-repeat: no-repeat; + background-position: center center; + background-size: 1.75ex 1.75ex; + height: 1.75ex; + width: 1.75ex; + content: ""; + display: inline; + margin-top: .7ex; + margin-left: 2px; + padding-left: 1.75ex; + vertical-align: middle; +} + +.usa-link--downloadable::after { + background: 0 0; + background-color: currentColor; + -webkit-mask-image: url(../img/material-icons/download.svg), linear-gradient(transparent, transparent); + mask-image: url(../img/material-icons/download.svg), linear-gradient(transparent, transparent); + -webkit-mask-position: center center; + mask-position: center center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: 1.75ex 1.75ex; + mask-size: 1.75ex 1.75ex; +} + +.usa-sidenav__sublist { + display: none; +} + +.usa-sidenav__item:hover .usa-sidenav__sublist, +.usa-sidenav__item:focus-within .usa-sidenav__sublist { + display: block; +} + +.usa-sidenav__item a { + display: block; +} diff --git a/app/config.py b/app/config.py index dece2728d..249451c66 100644 --- a/app/config.py +++ b/app/config.py @@ -87,6 +87,9 @@ class Config(object): "tts-benefits-studio@gsa.gov", ], } + FEATURE_BEST_PRACTICES_ENABLED = ( + getenv("FEATURE_BEST_PRACTICES_ENABLED", "false") == "true" + ) def _s3_credentials_from_env(bucket_prefix): diff --git a/app/main/views/index.py b/app/main/views/index.py index 4b0b57783..8b965991c 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -9,11 +9,27 @@ from app import redis_client, status_api_client from app.formatters import apply_html_class, convert_markdown_template from app.main import main from app.main.views.pricing import CURRENT_SMS_RATE -from app.main.views.sub_navigation_dictionaries import features_nav, using_notify_nav +from app.main.views.sub_navigation_dictionaries import ( + best_practices_nav, + features_nav, + using_notify_nav, +) from app.utils.user import user_is_logged_in from notifications_utils.url_safe_token import generate_token +# Hook to check for guidance routes +@main.before_request +def check_guidance_feature(): + current_app.logger.warning("best practices 1234") + current_app.logger.warning(current_app.config["FEATURE_BEST_PRACTICES_ENABLED"]) + if ( + request.path.startswith("/best-practices") + and not current_app.config["FEATURE_BEST_PRACTICES_ENABLED"] + ): + abort(404) + + @main.route("/") def index(): if current_user and current_user.is_authenticated: @@ -189,12 +205,78 @@ def trial_mode_new(): ) +@main.route("/best-practices") +@user_is_logged_in +def best_practices(): + return render_template( + "views/best-practices/best-practices.html", + navigation_links=best_practices_nav(), + ) + + +@main.route("/best-practices/clear-goals") +@user_is_logged_in +def clear_goals(): + return render_template( + "views/best-practices/clear-goals.html", + navigation_links=best_practices_nav(), + ) + + +@main.route("/best-practices/rules-and-regulations") +@user_is_logged_in +def rules_and_regulations(): + return render_template( + "views/best-practices/rules-and-regulations.html", + navigation_links=best_practices_nav(), + ) + + +@main.route("/best-practices/establish-trust") +@user_is_logged_in +def establish_trust(): + return render_template( + "views/best-practices/establish-trust.html", + navigation_links=best_practices_nav(), + ) + + +@main.route("/best-practices/write-for-action") +@user_is_logged_in +def write_for_action(): + return render_template( + "views/best-practices/write-for-action.html", + navigation_links=best_practices_nav(), + ) + + +@main.route("/best-practices/multiple-languages") +@user_is_logged_in +def multiple_languages(): + return render_template( + "views/best-practices/multiple-languages.html", + navigation_links=best_practices_nav(), + ) + + +@main.route("/best-practices/benchmark-performance") +@user_is_logged_in +def benchmark_performance(): + return render_template( + "views/best-practices/benchmark-performance.html", + navigation_links=best_practices_nav(), + ) + + @main.route("/using-notify/guidance") @user_is_logged_in def guidance_index(): return render_template( "views/guidance/index.html", navigation_links=using_notify_nav(), + feature_best_practices_enabled=current_app.config[ + "FEATURE_BEST_PRACTICES_ENABLED" + ], ) diff --git a/app/main/views/sub_navigation_dictionaries.py b/app/main/views/sub_navigation_dictionaries.py index 5c7cf26ba..0689c198d 100644 --- a/app/main/views/sub_navigation_dictionaries.py +++ b/app/main/views/sub_navigation_dictionaries.py @@ -58,3 +58,46 @@ def using_notify_nav(): # }, }, ] + + +def best_practices_nav(): + return [ + { + "name": "Best Practices", + "link": "main.best_practices", + }, + { + "name": "Clear goals", + "link": "main.clear_goals", + }, + { + "name": "Rules and regulations", + "link": "main.rules_and_regulations", + }, + { + "name": "Establish trust", + "link": "main.establish_trust", + "sub_navigation_items": [ + { + "name": "Get the word out", + "link": "main.establish_trust#get-the-word-out", + }, + { + "name": "As people receive texts", + "link": "main.establish_trust#as-people-receive-texts", + }, + ], + }, + { + "name": "Write for action", + "link": "main.write_for_action", + }, + { + "name": "Multiple languages", + "link": "main.multiple_languages", + }, + { + "name": "Benchmark performance", + "link": "main.benchmark_performance", + }, + ] diff --git a/app/templates/base.html b/app/templates/base.html index c535fafe2..369e62b81 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -53,9 +53,30 @@
{% if navigation_links %}
- {{ sub_navigation(navigation_links) }} +
-
+
{% else %}
{% endif %} diff --git a/app/templates/components/best-practices/cards.html b/app/templates/components/best-practices/cards.html new file mode 100644 index 000000000..1fa6f9a75 --- /dev/null +++ b/app/templates/components/best-practices/cards.html @@ -0,0 +1,42 @@ +
    + {% for item in card_contents %} + + {% endfor %} +
diff --git a/app/templates/components/best-practices/circle_number.html b/app/templates/components/best-practices/circle_number.html new file mode 100644 index 000000000..151dbca6d --- /dev/null +++ b/app/templates/components/best-practices/circle_number.html @@ -0,0 +1,9 @@ +{% macro circle_number(number) %} + + + + {{ number }} + + +{% endmacro %} diff --git a/app/templates/components/best-practices/nav_breadcrumb.html b/app/templates/components/best-practices/nav_breadcrumb.html new file mode 100644 index 000000000..fb5124356 --- /dev/null +++ b/app/templates/components/best-practices/nav_breadcrumb.html @@ -0,0 +1,13 @@ + diff --git a/app/templates/views/best-practices/benchmark-performance.html b/app/templates/views/best-practices/benchmark-performance.html new file mode 100644 index 000000000..2661f6b00 --- /dev/null +++ b/app/templates/views/best-practices/benchmark-performance.html @@ -0,0 +1,145 @@ +{% extends "base.html" %} + +{% set page_title = "Measuring performance with benchmarking" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +
+

{{page_title}}

+

Learn how effective your texting program can be.

+ +

+ Several factors determine a messaging campaign’s success, but the performance of previous projects can help set + expectations. Get to know what other government texting initiatives have done as a way to benchmark your own + performance. +

+

What other texting studies have found

+

+ When the Center on Budget and Policy Priorities studied WIC, they found key learnings about the + quantity of messages delivered, how people engage with messages, and how they take action. +

+ +

Message delivery

+

Benchmark: 80% of texts are + successfully delivered +

+
+
    +
  • +

    + You may discover that some numbers are temporarily or permanently unavailable due to service being + discontinued, + numbers changing, or being a landline. +

    +
  • +
+
+ + +

Engagement

+
+

Benchmark: Engagement rates with texts ranged from 17% + to 26%

+
+
+
    +
  • +

    + The highest rate of engagement with a text comes within hours of sending. Engagement rates include any kind + of + action taken due to a text, including replying “STOP” to prevent future texts. +

    +
  • +
+
+ +

Appointment requests

+
+

Benchmark: Requesting appointments after receiving texts ranged + from 4% to 9%

+
+
+
    +
  • +

    + Requesting appointments is a specific type of engagement. Provide a phone number or link to an online + appointment + request form. +

    +
  • +
+
+ +

+ The Code for America’s Texting + Playbook + reported specific learnings around appointment reminders, completing + document submission, and maintenance reminders. +

+

Appointment reminders

+
+

Benchmark: Clients were 79% more + likely + to keep + their appointment after receiving a text reminder.

+
+
+
    +
  • +

    You will likely see more completed appointments.

    +
  • +
+
+ +
+

Benchmark: Clients were 55% more + likely + to complete + an interview after receiving an interview reminder

+
+
+
    +
  • +

    You will likely see more completed interviews.

    +
  • +
+
+ +

Document submission

+
+

+ Benchmark: Clients were 6% more likely to complete document submission after + receiving a customized list of required documents via text +

+
+
+
    +
  • +

    To encourage response, provide a custom list of the needed documents and information about how to submit + them. +

    +
  • +
+
+ +

Reminders

+
+

Benchmark: Text reminders improved case maintenance rates by 21%

+
+
+
    +
  • +

    You may see less turnover in your case rates.

    +
  • +
+
+
+{% endblock %} diff --git a/app/templates/views/best-practices/best-practices.html b/app/templates/views/best-practices/best-practices.html new file mode 100644 index 000000000..a00a48660 --- /dev/null +++ b/app/templates/views/best-practices/best-practices.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} + +{% block per_page_title %} +Best Practices +{% endblock %} + +{% block content_column_content %} + +
+

Best Practices

+

For texting the public

+

Effectively reaching your audience and supporting your program’s goals starts with strategically planning out what + text messages can help you achieve and how to approach a thoughtful rollout. +

+ +

This set of best practices will help you get an effective texting initiative up and running.

+

+ Key elements of a texting campaign +

+ + {% set card_contents = [ + { + "svg_src": "goal", + "card_heading": "Establish clear goals", + "p_text": "Start with a singular purpose. Make explicit what you want to achieve.", + "link": "/best-practices/clear-goals" + }, + { + "svg_src": "compliant", + "card_heading": "Follow rules & regulations", + "p_text": "Understand what is required when texting the public.", + "link": "/best-practices/rules-and-regulations" + }, + { + "svg_src": "trust", + "card_heading": "Establish trust", + "p_text": "Help your audience anticipate and welcome your texts.", + "link": "/best-practices/establish-trust" + }, + { + "svg_src": "runner", + "card_heading": "Write texts that provoke action", + "p_text": "Help your audience know what to do with the information you send.", + "link": "/best-practices/write-for-action" + }, + { + "svg_src": "language", + "card_heading": "Send texts in multiple languages", + "p_text": "What to know as you plan translated texts.", + "link": "/best-practices/multiple-languages" + }, + { + "svg_src": "chart", + "card_heading": "Measure performance with benchmarking", + "p_text": "Learn how effective your texting program can be.", + "link": "/best-practices/benchmark-performance" + } + ] %} + + {% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{% + endwith %} + +
+{% endblock %} diff --git a/app/templates/views/best-practices/clear-goals.html b/app/templates/views/best-practices/clear-goals.html new file mode 100644 index 000000000..b67684c04 --- /dev/null +++ b/app/templates/views/best-practices/clear-goals.html @@ -0,0 +1,201 @@ +{% extends "base.html" %} + +{% set page_title = "Establish clear goals" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +
+

{{page_title}}

+

Start with a singular purpose. Make explicit what you want to achieve.

+
+
+

Text messaging should be one part of how you communicate with the people you serve, and it is best used to + provoke + action or influence behavior. Therefore, when starting to plan your texting campaign, it’s important to start + with + the end in mind. +

+
+
+

+ To establish clear goals, start by answering the following questions: +

+
+
    +
  • Who do you want to text?
  • +
  • At what moments do you want to text them?
  • +
  • Why do you want to text them?
  • +
  • What do you want them to do?
  • +
+
+
+
+
+
+
+
+ Phone +
+

Dept. of Human Services: We believe your EBT card may have been + used by + someone else. Please call the number on the back of your card (888-123-5555) to cancel and get a new + one. + If + you + need a new card immediately, please visit your local office. +

+
+
+
+
+
+ +
+

When should you send a text?

+
+
+
+
+

+ + What to do +

+
+

Use texting to share timely + information, provide critical status updates, or remind someone to do something.

+

Examples:

+ + {% set list_item_dos = [ + { + "list_content": "Your application is due on…", + }, + { + "list_content": "We need more information from you to process your application for…", + }, + { + "list_content": "We will be calling you about…at…", + }, + { + "list_content": "We will be calling you about…at…", + }, + ] %} +
    + {% for item in list_item_dos %} +
  • + {{item.list_content}} +
  • + {% endfor %} +
+
+
+
+
+
+
+
+

+ What not to do +

+
+

Texting is not great for long, + detailed instructions or frequent reminders that may be ignored. +

+

Examples:

+ {% set list_item_donts = [ + { + "list_content": "Your rights and responsibilities", + }, + { + "list_content": "How to complete your application", + }, + { + "list_content": "Weekly reminder not to share your account information", + }, + ] %} +
    + {% for item in list_item_donts %} +
  • + {{item.list_content}} +
  • + {% endfor %} +
+
+
+
+
+
+ +

Your program may already be communicating with your audience through methods like mailed notices and phone calls. + Think of text messages as complementary to your existing communications. Review how well you are currently reaching + your audience. Are there methods or gaps that could be supplemented with a text message? +

+

+ The Department of Veterans Affairs provides a helpful flow-chart that can help you decide if a text message is needed + for the communication problem you are trying to solve. +

+

Use a hypothesis framework to plan your campaign

+

+ Think of your texting campaign as a scientific experiment, and try to articulate what you hope to improve as a + result of implementing text messaging at your agency. For example: +

+
+
+
+

+ We believe that texting [specific group of people] when [this happens or on this + cadence] to inform/alert them about [subject] will result in [desired action]. +

+
+
+
+

+ Review your drafted hypothesis with your team to make sure everyone is aligned on your desired goals. A clear and + concise hypothesis can help you decide how to write text message + content + that provokes action. +

+

Begin with a small test

+

+ To help you decide how much to invest in text messaging over time, we highly recommend starting small and testing + your message campaigns to refine based on feedback and evidence. +

+

+ Starting small has less risk and more flexibility. It can help you gather data and decide if and how to send + more text messages. For example, start by sending one message to a smaller sample of your audience and track their + resulting behavior. +

+

+ At a minimum, make sure to get feedback on your message content from staff, community-based organizations, and, + ideally, from people who receive the texts themselves to ensure your messages are clear and actionable. +

+

Measure performance

+

Building and operationalizing a texting initiative requires time and effort, and you want to be able to demonstrate + the results of that effort.

+
+
+
+

+ To understand if your texting program is successful, ask: +

+
    +
  • What do you want to be different for your program or your desired audience after a text message is sent, + as compared to your current state?
  • +
  • How will you know if text recipients take the desired action?
  • +
  • Do you have access to existing data in your system that you could use to determine whether texting is + giving you the results you want?
  • +
+
+
+
+
+{% endblock %} diff --git a/app/templates/views/best-practices/establish-trust.html b/app/templates/views/best-practices/establish-trust.html new file mode 100644 index 000000000..fc26e2a71 --- /dev/null +++ b/app/templates/views/best-practices/establish-trust.html @@ -0,0 +1,193 @@ +{% extends "base.html" %} +{% from "components/best-practices/circle_number.html" import circle_number %} + +{% set page_title = "Establish trust" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +
+

{{page_title}}

+

Help your audience anticipate and welcome your texts.

+

+ People are wary of texts they’re not expecting to receive. Before you send your first text, consider how you will + gain trust with your audience and address any concerns of spam. Seize specific moments to reinforce what your + audience should expect from your messages. +

+

Get the word out

+

Before you launch, let your audience know you are adding texting as a communication method. People are more likely + to trust your text once they get it if they know that your program is sending texts.

+ +

How you currently communicate with your audience will determine the best approaches to announce your new + initiative. Harness channels you’re already using like social media, public websites, mailing inserts, or fliers at + key service locations as well as partnering with community organizations to get the word out about your new texting + program.

+

An example of key messages in direct mail

+
+ snap-renewal +
A paper mailer sent out by the State of Connecticut, in partnership with Code for America, to get the + word out about their new SNAP texting initiative. Flier displays an image of an introduction text and includes key + messages to reinforce legitimacy.
+
+ +

Prime your audiences to trust you

+
    +
  • +
    +

    To reinforce legitimacy, include these key messages in your outreach:

    +
      +
    1. Introduce yourself – Give people time to get to know your texting communications before you need + direct action from them
    2. +
    3. Phone number – Note the sending phone number your texts will come from
    4. +
    5. Text types – Be clear on the types of texts you intend to send, how often, and why they matter
    6. +
    7. Privacy policy– For example, state that you will not collect personal information, including the + types of information you will never ask for
    8. +
    9. Places to verify – Include a phone number, website, or human where people can verify that the texts + are coming from you
    10. +
    11. Opt out – Provide opt-out instructions and a disclaimer for the potential cost of receiving the + text +
    12. +
    +
    +
  • +
  • +
    + Phone +
    +

    Dept. of Social Services: Welcome to text updates from the + {{circle_number(1)}}Department of Social Services. {{ circle_number(2) }}We’ll always text + you from + this number, save us to your contact list. Our goal is to {{ circle_number(3) }} keep you up to + date on + your account status. {{ circle_number(4) }} We will never ask for personal details in a text. + {{ circle_number(5) }} If you have questions about how we protect your privacy, see + state.gov/privacy.{{circle_number(6) }}Reply STOP to stop. +

    +
    +
    +
  • +
+
+

As people receive texts

+

When a person has questions or needs assistance, reaching out for support may be natural for them. Give direction + in your texts so your audience understands what to do with the information you provide. Remember, it’s important to + repeat consistent and standardized language across texts and programs.

+

Be trustworthy

+
    +
  • +
    +
      +
    1. Identify yourself in the text message. Use a consistent and familiar program name to precede each + text you send.
    2. +
    3. Personalize with first name. Through A/B testing, Code for America saw an increase in + follow-through by 25% if someone’s first name was included.
    4. +
    5. Use full and direct .gov website URLs. Avoid link shorteners, as they obscure your links and + potentially make it easier for a malicious actor to spoof your text.
    6. +
    7. If providing a phone number, use a familiar phone number and provide information on how to verify + its legitimacy.
    8. +
    +
    +
  • +
  • +
    + Phone +
    +

    + {{circle_number(7) }}Dept. of Social Services: Hi {{circle_number(8) }}Julie, Your Medicaid + renewal is closing December 31, 2023. You can renew online at {{circle_number(9) }}https://www.application.yourstate.gov or {{circle_number(10) }} + call the number on the back of your Medicaid card. +

    +
    +
    +
  • +
+ +

Provide helpful auto-responses

+
    +
  • +
    +

    If you're sending one-way notifications, phone carriers allow a single auto-response message that will be + generated if a recipient tries to text a response to your message. Use the auto-response to reaffirm your key + messages around legitimacy and communicate to recipients that texts are coming from an automated system.

    +
      +
    1. Provide a way to contact a human in any auto-response text
    2. +
    3. Provide information on how to opt out and opt back in
    4. +
    +
    +
  • +
  • +
    + Phone +
    +

    + Dept. of Social Services: This number is unmonitored. To {{circle_number(11) }} contact us, call + 888-123-4567. We will never ask for personal details in a text. If you have questions about how to + protect your privacy, see state.gov/privacy {{circle_number(12) }} Reply “STOP” to this phone number + to opt out of receiving texts, and “START” to opt back in. +

    +
    +
    +
  • +
+ +

Be prepared for questions

+

+ Your texting program will only be perceived as legitimate if you get the word out in many different ways. It's also + important that agency employees who support text recipients (for example, the call center) are familiar with the + texting program. They can accurately respond to recipient questions about them, so make sure you’re priming your + delivery staff, especially those who interact directly with your intended audience. +

+ +

Harness trusted communication channels

+ {% set card_contents = [ + { + "image_src": asset_url('images/website.svg'), + "card_heading": "Websites", + "alt_text": "Graphic representing a website browser", + "p_text": "Post banner alerts about texting on your agency website and link to a press release for more information. + Publish a public texting privacy policy.", + }, + { + "image_src": asset_url('images/community-org.svg'), + "card_heading": "Community organizations", + "alt_text": "Illustration of community organizations spreading awareness", + "p_text": "Alert trusted community-based organizations that texts are coming and what they look like. Equip them with + fliers, posters, and talking points to spread the word.", + }, + { + "image_src": asset_url('images/call-center.svg'), + "card_heading": "IVR / Call Centers", + "alt_text": "Illustration of a call center promoting texting initiative", + "p_text": "Consider adding hold message announcements about your texting initiative to further get the word out.", + }, + ] %} + + {% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{% + endwith %} +

Prepare your team

+ {% set card_contents = [ + { + "image_src": asset_url('images/grouping.svg'), + "card_heading": "Staff", + "alt_text": "Illustration of staff group briefing", + "p_text": "Give all staff a heads-up about texts before and when they are sent. Share talking points on what texts + you’re sending, when you’ll start to send them, the phone number they are coming from, and where staff can escalate + any issues.", + }, + { + "image_src": asset_url('images/project-management.svg'), + "card_heading": "Case management systems", + "alt_text": "Graphic representing project management", + "p_text": "When it makes sense, include information about texts being sent to specific clients on individual splash + pages or within case management notes.", + }, + ] %} + {% with card_contents=card_contents %}{% include "components/best-practices/cards.html" %}{% + endwith %} +
+{% endblock %} diff --git a/app/templates/views/best-practices/multiple-languages.html b/app/templates/views/best-practices/multiple-languages.html new file mode 100644 index 000000000..13c638d38 --- /dev/null +++ b/app/templates/views/best-practices/multiple-languages.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% set page_title = "Text in multiple languages" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +
+

{{page_title}}

+

What to know as you plan translated texts.

+ +

+ Sending messages in recipients’ preferred language demonstrates respect, makes it more likely that recipients will + understand and take action on the message, and may be required by your agency or program. +

+ +

+ To send translated messages using Notify.gov you will need two things: a record of your recipients’ preferred + language and translation/s of the message template. Notify.gov does not provide translation services. +

+ +

+ As with messages in English, we strongly recommend user testing the translations with people who speak the language + to ensure the messages are understandable. +

+
+{% endblock %} diff --git a/app/templates/views/best-practices/rules-and-regulations.html b/app/templates/views/best-practices/rules-and-regulations.html new file mode 100644 index 000000000..c9a95a122 --- /dev/null +++ b/app/templates/views/best-practices/rules-and-regulations.html @@ -0,0 +1,126 @@ +{% extends "base.html" %} + +{% set page_title = "Rules and regulations" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +
+

{{page_title}}

+

Understand what is required when texting the public.

+

What to know about consent and opting out

+

+ Providing consent and opting out are key ways for text recipients to express their text communication preferences. + Discuss your obligation to obtain consent with your legal counsel. +

+

+ If you do need expressed consent, consider including a pre-checked plain language opt-in (i.e. “It’s OK to text + me.”) on + digital forms. Be sure to ask for an up-to-date phone number and include a question about the recipient’s preferred + language for text messages if you expect to translate your text + messages in + languages other than English. +

+ {% set links = [ + { + "p_text": 'The Telephone Consumer + Protection Act (TCPA) (47 USC § 227) is the federal law that impacts how organizations are allowed to + communicate + in bulk with the public via telephone (including text message or SMS).' + }, + { + "p_text": 'The FCC has ruled that Federal and State programs are exempt from the TCPA and can + send + text messages to the public without consent if conducting official business. Without explicit mention in the ruling, + local governments, phone carriers, or any texting intermediaries might require it.' + }, + { + "url_link": + "pdf/tcpa_overview.pdf", + "url_text":"Download and share our overview of the TCPA with your legal counsel", + "is_downloadable": true, + "p_text": 'It provides a baseline + interpretation to aid your legal counsel in getting up to speed with what might be needed for your program or use + case. + For additional questions about the enforcement of the TCPA, you can watch a recorded training on public + benefits texting provided by the FCC.' + }, + ] %} +

Complying with the Telephone Consumer Protection Act (TCPA)

+
    + {% for item in links %} +
  • +
    + {% if item.url_text %} +

    + + {{ item.url_text }} + +

    + {% endif %} +

    + {{item.p_text |safe}} +

    +
    +
  • + {% endfor %} + +
+

Opting out

+

+ There is no policy requirement for senders to communicate opt-out options, but including instructions in introductory and/or + auto-response texts on how to opt out and opt back in are effective ways to establish trust with your + audience. +

+ +

+ In the US, no matter where texts are coming from, a recipient has the right to revoke consent at any time through + any + reasonable means. For example, a text recipient can always opt out of receiving text messages from a phone number by + responding “STOP”, “QUIT”, or one of several other keywords. +

+ +

+ Any subsequent messages sent to that number will be blocked by the phone carrier and not delivered. We recommend + establishing an internal process to obtain and remove numbers that fail multiple times to avoid continuing to send + messages to them. +

+

Honoring privacy

+

Phone numbers are personally identifiable information (PII)

+

+ When putting together the data spreadsheet (or CSV) file containing text message data, follow relevant security + processes for storing and transmitting the file that contains PII. +

+

Don’t text sensitive information

+

+ Sensitive information should be protected and secure. Social Security Numbers, driver’s license numbers, personal + health information, and passwords are all sensitive PII that should not be shared via text message. Consult with + your privacy office to understand what information you can and cannot include in a text message. Different programs + and jurisdictions may have different rules and interpretations. +

+

Assess risk

+

+ Consider the risks if someone who is not the intended recipient receives the text message. Establish an internal + process + to keep your phone number list up-to-date. The FCC offers a Reassigned + Numbers Database to identify phone numbers that may have been reassigned since your agency obtained them. +

+
+ + +{% endblock %} diff --git a/app/templates/views/best-practices/write-for-action.html b/app/templates/views/best-practices/write-for-action.html new file mode 100644 index 000000000..d97b74d8b --- /dev/null +++ b/app/templates/views/best-practices/write-for-action.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% from "components/best-practices/circle_number.html" import circle_number %} + +{% set page_title = "Write texts that provoke action" %} + +{% block per_page_title %} +{{page_title}} +{% endblock %} + +{% block content_column_content %} +{% with title=page_title %}{% include "components/best-practices/nav_breadcrumb.html" %}{% endwith %} +
+

{{page_title}}

+

Help your audience know what to do with the information you send.

+

+ When writing a text, focus on the person who is going to receive the message and write it for them. Use plain + language and provide easy to understand next steps. +

+
+
+

+ When deciding what types of messages to draft for your campaign, think through the following questions: +

+
+
    +
  • What do you want recipients to do when they receive the text?
  • +
  • What will happen if a recipient doesn't take the action you want them to take?
  • +
  • Will you send any follow-up texts depending on if the action was taken or not?
  • +
+
+
+
+

Be clear and concise

+
    +
  • Use simple, everyday language. Keep the text short and clear. Stay under 300 characters.
  • +
  • + Say only one important thing per message. Focus on the most essential instructions and information. If there are + multiple actions you want someone to take, consider sending a series of well-timed messages. +
  • +
  • Use a neutral, direct, and professional tone. This works better than a friendly or overly casual tone.
  • +
+

Build conditions for action

+
    +
  • +
    +

    For example, getting a person to update their mailing address:

    +
      +
    1. Clearly state the information and the response you want the recipient to take.
    2. +
    3. Point directly to where the action can be completed, not to more information.
    4. +
    5. Make sure the action can be completed via mobile phone, like calling a person or going to a + mobile-friendly URL. +
    6. +
    7. + Include at least two options for a recipient to take action. Some recipients may be more comfortable talking + directly + to a person, while others may prefer a web-based experience. +
    8. +
    9. Consider clearly explaining any consequences if the desired action is not completed.
    10. +
    +
    +
  • +
  • +
    + Phone +
    +

    {{circle_number(1) }}State Medicare Agency: Hello, MEDICARE has + attempted to + send you an update about your claim, but the mail was returned.
    {{circle_number(2) }}Please go to + https://www.medicare.gov/account/login?utm_source=TEXT, {{circle_number(3) }}log in to your account, and + click “My + Account Information” to verify that your address is correct. If you are having difficulty logging in, + {{circle_number(4) }}call 1-800-MEDICARE for assistance. {{circle_number(5) }}We will attempt to send the + mail again + in 14 days. +

    +
    +
    +
  • +
+

What provoking action looks like

+

Evidence + shows that employing + behavioral science is an effective way to increase the + likelihood of a recipient + taking action upon receiving your text. Keep in mind these three foundational principles while drafting your text + message:

+ {% set card_contents = [ + { + "card_heading": "Feeling a sense of ownership can influence behavior.", + "p_text": "Example – Don't miss this chance to get free internet or $30 per month off your internet bill.", + }, + { + "card_heading": "Individuals are strongly influenced by others’ behaviors.", + "p_text": "Example – Thousands of residents already get an + average of $113/month in money for food.", + }, + { + "card_heading": "Individuals perceive losses more powerfully than gains.", + "p_text": "Example – Contact us for potentially unclaimed free + internet services that belong to you.", + }, + ] %} + {% with card_contents=card_contents, text_align='left' %}{% include "components/best-practices/cards.html" %}{% + endwith %} +
+{% endblock %} diff --git a/app/templates/views/guidance/index.html b/app/templates/views/guidance/index.html index a7e4d496f..2e63c6212 100644 --- a/app/templates/views/guidance/index.html +++ b/app/templates/views/guidance/index.html @@ -16,17 +16,22 @@

Below we explain how to:

{# Format content #}

Format your content

+ {% if not feature_best_practices_enabled %}

Effective texts will help your message recipients take the steps needed to secure and keep the benefits and services they depend on. To craft an effective text:

@@ -37,7 +42,7 @@
  • Say only one important thing per message.
  • Frame the message to encourage action and explain the consequences of not completing the desired action.
  • - + {% endif %}

    To create and format your message

    1. All messages start from a template
    2. @@ -45,6 +50,7 @@
    3. Add a new template or choose an existing template and select Edit.
    + {% if not feature_best_practices_enabled %} {# Add links #}

    When composing a text message, links to websites or online applications can help your recipient respond quickly.

    @@ -64,6 +70,7 @@
  • Your link might stop working if there’s a service outage.
  • You can no longer control where the redirect goes.
  • + {% endif %} {# Personalize content #}

    Personalize your content

    @@ -214,6 +221,7 @@
    + {% if not feature_best_practices_enabled %} {# Preventing fraud #}

    Preventing fraud

    Texting fraud is ever prevalent and while we can’t eliminate the risk, we can reduce the chances that recipients will @@ -240,5 +248,5 @@ in a text. If you have questions about how to protect your privacy, see statename.gov/privacy.

    - + {% endif %} {% endblock %} diff --git a/app/templates/views/roadmap.html b/app/templates/views/roadmap.html index 97f982e12..f9446aad5 100644 --- a/app/templates/views/roadmap.html +++ b/app/templates/views/roadmap.html @@ -33,7 +33,7 @@
    • Achieve compliance to begin piloting, such as ATO and privacy standards.
    • Demonstrate that a government-run notifications tool provides a unique value.
    • diff --git a/backstop.config.js b/backstop.config.js index 09e926cf6..5e5df60fa 100644 --- a/backstop.config.js +++ b/backstop.config.js @@ -1,6 +1,6 @@ const { urls, baseUrl } = require('./urls'); -const MISMATCH_THRESHOLD = 0.2; +const MISMATCH_THRESHOLD = 0.3; const SCREENSHOT_DELAY = 2000; const createScenariosFromUrls = (urls, delay = SCREENSHOT_DELAY) => { diff --git a/backstop_data/bitmaps_reference/backstop_test_Add_Service_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Add_Service_0_document_0_desktop.png index b16084922..c08a67eb2 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Add_Service_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Add_Service_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Benchmark_Performance_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Benchmark_Performance_0_document_0_desktop.png new file mode 100644 index 000000000..02cba0378 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_Benchmark_Performance_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_0_h1heading-large_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_0_h1heading-large_0_desktop.png index 5597ee2cf..a3370c433 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_0_h1heading-large_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_0_h1heading-large_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_1_ausa-buttonhrefadd-service_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_1_ausa-buttonhrefadd-service_0_desktop.png index df8b6ccc3..e65896fd6 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_1_ausa-buttonhrefadd-service_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Choose_Service_-_Accounts_1_ausa-buttonhrefadd-service_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Clear_Goals_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Clear_Goals_0_document_0_desktop.png new file mode 100644 index 000000000..02cba0378 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_Clear_Goals_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Delivery_Status_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Delivery_Status_0_document_0_desktop.png index a6ba64bae..c774905ce 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Delivery_Status_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Delivery_Status_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Establish_Trust_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Establish_Trust_0_document_0_desktop.png new file mode 100644 index 000000000..02cba0378 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_Establish_Trust_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Features_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Features_0_document_0_desktop.png index dfd78f629..a9f4e51dd 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Features_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Features_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Get_Started_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Get_Started_0_document_0_desktop.png index a84d9db34..899bcf30f 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Get_Started_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Get_Started_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Get_Started_Page_-_Highlight_Trial_Mode_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Get_Started_Page_-_Highlight_Trial_Mode_0_document_0_desktop.png index a84d9db34..899bcf30f 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Get_Started_Page_-_Highlight_Trial_Mode_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Get_Started_Page_-_Highlight_Trial_Mode_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Guidance_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Guidance_0_document_0_desktop.png index 9ab3a9074..02cba0378 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Guidance_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Guidance_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Homepage_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Homepage_0_document_0_desktop.png index bf1886eca..68997e23a 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Homepage_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Homepage_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Multiple_Languages_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Multiple_Languages_0_document_0_desktop.png new file mode 100644 index 000000000..02cba0378 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_Multiple_Languages_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Pricing_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Pricing_0_document_0_desktop.png index 383b18c5e..b494fe476 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Pricing_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Pricing_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Roadmap_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Roadmap_0_document_0_desktop.png index c4cf0a512..df7902bd2 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Roadmap_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Roadmap_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Rules_And_Regulations_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Rules_And_Regulations_0_document_0_desktop.png new file mode 100644 index 000000000..02cba0378 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_Rules_And_Regulations_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Security_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Security_0_document_0_desktop.png index 4d0af6df7..17e172fa8 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Security_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Security_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Support_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Support_0_document_0_desktop.png index e0ce95c8d..b1045ba72 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Support_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Support_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Trial_Mode_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Trial_Mode_0_document_0_desktop.png index f8aef5aee..f1196ee79 100644 Binary files a/backstop_data/bitmaps_reference/backstop_test_Trial_Mode_0_document_0_desktop.png and b/backstop_data/bitmaps_reference/backstop_test_Trial_Mode_0_document_0_desktop.png differ diff --git a/backstop_data/bitmaps_reference/backstop_test_Write_For_Action_0_document_0_desktop.png b/backstop_data/bitmaps_reference/backstop_test_Write_For_Action_0_document_0_desktop.png new file mode 100644 index 000000000..02cba0378 Binary files /dev/null and b/backstop_data/bitmaps_reference/backstop_test_Write_For_Action_0_document_0_desktop.png differ diff --git a/deploy-config/demo.yml b/deploy-config/demo.yml index a003a32ea..05d167f4b 100644 --- a/deploy-config/demo.yml +++ b/deploy-config/demo.yml @@ -1,8 +1,10 @@ env: demo instances: 1 memory: 1G +command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: notify-demo.app.cloud.gov cloud_dot_gov_route: notify-demo.app.cloud.gov redis_enabled: 1 -nr_agent_id: "1134302465" -nr_app_id: "1083160688" +nr_agent_id: '1134302465' +nr_app_id: '1083160688' +FEATURE_BEST_PRACTICES_ENABLED: true diff --git a/deploy-config/production.yml b/deploy-config/production.yml index b99c4dd54..42a681cb4 100644 --- a/deploy-config/production.yml +++ b/deploy-config/production.yml @@ -1,8 +1,10 @@ env: production instances: 2 memory: 1.5G +command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: beta.notify.gov cloud_dot_gov_route: notify.app.cloud.gov redis_enabled: 1 -nr_agent_id: "1050708682" -nr_app_id: "1050708682" +nr_agent_id: '1050708682' +nr_app_id: '1050708682' +FEATURE_BEST_PRACTICES_ENABLED: false diff --git a/deploy-config/sandbox.yml b/deploy-config/sandbox.yml index 71b51dbd6..5cee4f053 100644 --- a/deploy-config/sandbox.yml +++ b/deploy-config/sandbox.yml @@ -1,6 +1,7 @@ env: sandbox instances: 1 memory: 1G +command: gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: notify-sandbox.app.cloud.gov cloud_dot_gov_route: notify-sandbox.app.cloud.gov redis_enabled: 1 @@ -8,6 +9,7 @@ ADMIN_CLIENT_USERNAME: notify-admin ADMIN_CLIENT_SECRET: sandbox-notify-secret-key DANGEROUS_SALT: sandbox-notify-salt SECRET_KEY: sandbox-notify-secret-key -nr_agent_id: "" -nr_app_id: "" -NR_BROWSER_KEY: "" +nr_agent_id: '' +nr_app_id: '' +NR_BROWSER_KEY: '' +FEATURE_BEST_PRACTICES_ENABLED: true diff --git a/deploy-config/staging.yml b/deploy-config/staging.yml index f16669da2..036f4472e 100644 --- a/deploy-config/staging.yml +++ b/deploy-config/staging.yml @@ -1,8 +1,10 @@ env: staging instances: 1 memory: 1G +command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application public_admin_route: notify-staging.app.cloud.gov cloud_dot_gov_route: notify-staging.app.cloud.gov redis_enabled: 1 -nr_agent_id: "1134291385" -nr_app_id: "1031640326" +nr_agent_id: '1134291385' +nr_app_id: '1031640326' +FEATURE_BEST_PRACTICES_ENABLED: false diff --git a/docs/feature-flagging.md b/docs/feature-flagging.md new file mode 100644 index 000000000..ecefa2250 --- /dev/null +++ b/docs/feature-flagging.md @@ -0,0 +1,56 @@ +# Feature Flagging + +## Using Environment Variables + +This guide explains how to update cloud foundry envirionment variables to enable or disable feature flagging. + +### Prerequisites + +- Cloud Foundry CLI (`cf`) must be installed. +- Access to Cloud Foundry with the necessary credentials. + +## Steps to Update + +### 1. Log in to Cloud Foundry + +```bash +cf login -a api.fr.cloud.gov --sso +``` + +### 2. Target Correct Space + +This should be handled after authenticating but if not, you can target the spaces specifically + +``` +cf target -o gsa-tts-benefits-studio -s notify-sandbox +``` + +### 3. Set Environment Variable + +``` +cf set-env +``` + +#### Example: + +``` +cf set-env notify-admin-sandbox FEATURE_BEST_PRACTICES_ENABLED true +``` + +### 4. Restage the Application + +``` +cf restage +``` + +#### Example: + +``` +cf restage notify-admin-sandbox +``` + +### 5. Update environment specific manifest file(s) and merge into source code + +#### Example: + +If demo environment had a feature flag flipped, then navigate to the demo.yml file and add appropriate value. diff --git a/gulpfile.js b/gulpfile.js index 7da952589..068643e74 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -112,11 +112,19 @@ const copyImages = () => { ); }; +// Task to pdf files +const copyPDF = () => { + return src(paths.src + 'pdf/**/*', { encoding: false }).pipe( + dest(paths.dist + 'pdf/') + ); +}; + // Configure USWDS paths uswds.settings.version = 3; uswds.paths.dist.css = paths.dist + 'css'; uswds.paths.dist.js = paths.dist + 'js'; uswds.paths.dist.img = paths.dist + 'img'; +uswds.paths.dist.pdf = paths.dist + 'pdf'; uswds.paths.dist.fonts = paths.dist + 'fonts'; uswds.paths.dist.theme = paths.src + 'sass/uswds'; @@ -163,6 +171,7 @@ exports.default = series( copyGtmHead, copySetTimezone, copyImages, + copyPDF, copyAssets ); exports.backstopTest = backstopTest; diff --git a/manifest.yml b/manifest.yml index b076b3b47..3e5f13b4c 100644 --- a/manifest.yml +++ b/manifest.yml @@ -5,7 +5,7 @@ applications: stack: cflinuxfs4 instances: ((instances)) memory: ((memory)) - command: newrelic-admin run-program gunicorn -c /home/vcap/app/gunicorn_config.py application + command: ((command)) health-check-type: port health-check-invocation-timeout: 10 routes: @@ -21,7 +21,7 @@ applications: NOTIFY_APP_NAME: admin NOTIFY_LOG_LEVEL: INFO FLASK_APP: application.py - FLASK_DEBUG: "false" + FLASK_DEBUG: 'false' NEW_RELIC_CONFIG_FILE: newrelic.ini NEW_RELIC_ENVIRONMENT: ((env)) @@ -44,8 +44,8 @@ applications: NOTIFY_BILLING_DETAILS: '[]' - REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt" - NEW_RELIC_CA_BUNDLE_PATH: "/etc/ssl/certs/ca-certificates.crt" + REQUESTS_CA_BUNDLE: '/etc/ssl/certs/ca-certificates.crt' + NEW_RELIC_CA_BUNDLE_PATH: '/etc/ssl/certs/ca-certificates.crt' COMMIT_HASH: ((COMMIT_HASH)) @@ -60,3 +60,6 @@ applications: LOGIN_DOT_GOV_SIGNOUT_REDIRECT: ((LOGIN_DOT_GOV_SIGNOUT_REDIRECT)) LOGIN_DOT_GOV_INITIAL_SIGNIN_URL: ((LOGIN_DOT_GOV_INITIAL_SIGNIN_URL)) LOGIN_DOT_GOV_CERTS_URL: ((LOGIN_DOT_GOV_CERTS_URL)) + + # feature flagging + FEATURE_BEST_PRACTICES_ENABLED: ((FEATURE_BEST_PRACTICES_ENABLED)) diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index a4d57a89c..27e7f074e 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -2,7 +2,7 @@ from functools import partial import pytest from bs4 import BeautifulSoup -from flask import url_for +from flask import current_app, url_for from freezegun import freeze_time from tests.conftest import SERVICE_ONE_ID, normalize_spaces @@ -49,6 +49,8 @@ def test_logged_in_user_redirects_to_choose_account( "main.index", _expected_status=302, ) + + # Modify expected URL to include the query parameter client_request.get( "main.sign_in", _expected_status=302, @@ -91,6 +93,13 @@ def test_hiding_pages_from_search_engines( "roadmap", "features", "documentation", + "best_practices", + "clear_goals", + "rules_and_regulations", + "establish_trust", + "write_for_action", + "multiple_languages", + "benchmark_performance", "security", "message_status", "features_sms", @@ -105,26 +114,49 @@ def test_hiding_pages_from_search_engines( ) def test_static_pages(client_request, mock_get_organization_by_domain, view, mocker): mocker.patch("app.notify_client.user_api_client.UserApiClient.deactivate_user") + + # Function to check if a view is feature-flagged and should return 404 when disabled + def is_feature_flagged(view): + feature_flagged_views = [ + "best_practices", + "clear_goals", + "rules_and_regulations", + "establish_trust", + "write_for_action", + "multiple_languages", + "benchmark_performance", + ] + return ( + not current_app.config["FEATURE_BEST_PRACTICES_ENABLED"] + and view in feature_flagged_views + ) + request = partial(client_request.get, "main.{}".format(view)) - # Check the page loads when user is signed in - page = request() - assert page.select_one("meta[name=description]") + # If the guidance feature is disabled, expect a 404 for feature-flagged views + if is_feature_flagged(view): + page = request(_expected_status=404) + else: + # Check the page loads when user is signed in + page = request() + assert page.select_one("meta[name=description]") - # Check it still works when they don’t have a recent service - with client_request.session_transaction() as session: - session["service_id"] = None - request() + # Check it still works when they don’t have a recent service + with client_request.session_transaction() as session: + session["service_id"] = None + request() - # Check it redirects to the login screen when they sign out - client_request.logout() - with client_request.session_transaction() as session: - session["service_id"] = None - session["user_id"] = None - request( - _expected_status=302, - _expected_redirect="/sign-in?next={}".format(url_for("main.{}".format(view))), - ) + # Check it redirects to the login screen when they sign out + client_request.logout() + with client_request.session_transaction() as session: + session["service_id"] = None + session["user_id"] = None + request( + _expected_status=302, + _expected_redirect="/sign-in?next={}".format( + url_for("main.{}".format(view)) + ), + ) def test_guidance_pages_link_to_service_pages_when_signed_in(client_request, mocker): @@ -184,31 +216,6 @@ def test_old_using_notify_page(client_request): client_request.get("main.using_notify", _expected_status=410) -# def test_old_integration_testing_page( -# client_request, -# ): -# page = client_request.get( -# 'main.integration_testing', -# _expected_status=410, -# ) -# assert normalize_spaces(page.select_one('.grid-row').text) == ( -# 'Integration testing ' -# 'This information has moved. ' -# 'Refer to the documentation for the client library you are using.' -# ) -# assert page.select_one('.grid-row a')['href'] == url_for( -# 'main.documentation' -# ) - - -# def test_terms_page_has_correct_content(client_request): -# terms_page = client_request.get("main.terms") -# assert normalize_spaces(terms_page.select("main p")[0].text) == ( -# "These terms apply to your service’s use of Notify.gov. " -# "You must be the service manager to accept them." -# ) - - def test_css_is_served_from_correct_path(client_request): page = client_request.get("main.documentation") # easy static page diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 527e85554..8bf52d803 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -33,6 +33,8 @@ EXCLUDED_ENDPOINTS = tuple( "archive_service", "archive_user", "begin_tour", + "benchmark_performance", + "best_practices", "billing_details", "callbacks", "cancel_invited_org_user", @@ -50,6 +52,7 @@ EXCLUDED_ENDPOINTS = tuple( "choose_template", "choose_template_to_copy", "clear_cache", + "clear_goals", "confirm_edit_user_email", "confirm_edit_user_mobile_number", "confirm_redact_template", @@ -87,6 +90,7 @@ EXCLUDED_ENDPOINTS = tuple( "edit_user_permissions", "email_not_received", "error", + "establish_trust", "features", "features_sms", "find_services_by_name", @@ -127,6 +131,7 @@ EXCLUDED_ENDPOINTS = tuple( "manage_users", "message_status", "monthly", + "multiple_languages", "new_password", "notifications_sent_by_service", "old_guest_list", @@ -161,6 +166,7 @@ EXCLUDED_ENDPOINTS = tuple( "revalidate_email_sent", "revoke_api_key", "roadmap", + "rules_and_regulations", "security", "security_policy", "send_files_by_email", @@ -250,6 +256,7 @@ EXCLUDED_ENDPOINTS = tuple( "view_template_version", "view_template_versions", "who_its_for", + "write_for_action", }, ) ) diff --git a/urls.js b/urls.js index 4a84cac47..8ff543470 100644 --- a/urls.js +++ b/urls.js @@ -13,6 +13,13 @@ const sublinks = [ { label: 'Roadmap', path: '/features/roadmap' }, { label: 'Security', path: '/features/security' }, { label: 'Support', path: '/support' }, + { label: 'Best Practices', path: '/best-practices' }, + { label: 'Clear Goals', path: '/best-practices/clear-goals' }, + { label: 'Rules And Regulations', path: '/best-practices/rules-and-regulations' }, + { label: 'Establish Trust', path: '/best-practices/establish-trust' }, + { label: 'Write For Action', path: '/best-practices/write-for-action' }, + { label: 'Multiple Languages', path: '/best-practices/multiple-languages' }, + { label: 'Benchmark Performance', path: '/best-practices/benchmark-performance' }, // Add more links here as needed ];