Merge branch 'main' into 2073---new-best-practice-side-nav-end2end

This commit is contained in:
Beverly Nguyen
2024-10-31 09:54:34 -07:00
59 changed files with 1602 additions and 65 deletions

View File

@@ -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"
}

1
.gitignore vendored
View File

@@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 563 670" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M553.831 334.751V335.974L555.03 335.731C555.104 335.716 555.181 335.708 555.262 335.708H557.846C558.483 335.708 559 336.224 559 336.862V418.708C559 419.345 558.483 419.862 557.846 419.862H555.262C555.181 419.862 555.104 419.853 555.03 419.838L553.831 419.595V420.818V955.446C553.831 970.12 547.528 991.015 535.312 1008.3C522.978 1025.76 503.794 1040.6 477.723 1040.6H82.2769C56.206 1040.6 37.022 1025.76 24.6885 1008.3C12.472 991.015 6.16923 970.12 6.16923 955.446V420.818V419.595L4.97026 419.838C4.89621 419.853 4.8188 419.862 4.73846 419.862H2.15385C1.51659 419.862 1 419.345 1 418.708V358.4C1 357.763 1.51659 357.246 2.15385 357.246H4.73846C4.8188 357.246 4.89621 357.254 4.97026 357.269L6.16923 357.513V356.289V347.588V346.364L4.97026 346.608C4.89621 346.623 4.8188 346.631 4.73846 346.631H2.15385C1.51659 346.631 1 346.114 1 345.477V285.169C1 284.532 1.51659 284.015 2.15385 284.015H4.73846C4.8188 284.015 4.89621 284.023 4.97026 284.038L6.16923 284.282V283.058V261.434V260.21L4.97026 260.454C4.89621 260.469 4.8188 260.477 4.73846 260.477H2.15385C1.51659 260.477 1 259.96 1 259.323V220.554C1 219.917 1.51659 219.4 2.15385 219.4H4.73846C4.8188 219.4 4.89624 219.408 4.97036 219.423L6.16923 219.666V218.443V86.1538C6.16923 71.4804 12.472 50.5848 24.6885 33.2974C37.022 15.8443 56.206 1 82.2769 1H477.723C503.794 1 522.978 15.8443 535.312 33.2974C547.528 50.5848 553.831 71.4804 553.831 86.1538V334.751ZM400.615 16.2308H399.615V17.2308V43.0769C399.615 49.6619 394.277 55 387.692 55H172.308C165.723 55 160.385 49.6619 160.385 43.0769V17.2308V16.2308H159.385H82.2769C62.6883 16.2308 47.6257 27.2304 37.1269 42.0872C26.5125 57.1076 21.4 74.9813 21.4 86.1538V955.446C21.4 966.619 26.5125 984.492 37.1269 999.513C47.6257 1014.37 62.6883 1025.37 82.2769 1025.37H477.723C497.312 1025.37 512.374 1014.37 522.873 999.513C533.487 984.492 538.6 966.619 538.6 955.446V86.1538C538.6 74.9813 533.488 57.1076 522.873 42.0872C512.374 27.2304 497.312 16.2308 477.723 16.2308H400.615Z" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -0,0 +1 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><g><g><path d="m291.532 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m254.455 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m217.378 66.273c4.142 0 7.5-3.357 7.5-7.5v-.057c0-4.143-3.358-7.472-7.5-7.472s-7.5 3.386-7.5 7.528 3.358 7.501 7.5 7.501z"/><path d="m123.925 51.273-76.763-.057c-.002 0-.003 0-.005 0-4.14 0-7.497 3.354-7.5 7.494-.003 4.143 3.353 7.503 7.495 7.506l76.763.057h.005c4.14 0 7.497-3.354 7.5-7.494.002-4.142-3.353-7.503-7.495-7.506z"/><path d="m287.507 124.863h-111.062c-6.933 0-12.574 5.641-12.574 12.574v33.479c0 6.948 5.641 12.602 12.574 12.602h111.062c6.933 0 12.574-5.653 12.574-12.602v-33.479c0-6.933-5.641-12.574-12.574-12.574zm-2.426 43.655h-106.209v-28.654h106.209z"/><path d="m292.581 210.217h-121.209c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h121.209c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m189.882 245.736h-18.51c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h18.51c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m292.581 245.736h-62.05c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h62.05c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m444.802 450.744h-.255c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h.255c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5z"/><path d="m490.667 301.75h-59.114v-129.049c0-11.669-9.494-21.163-21.163-21.163h-71.7v-114.592c0-11.404-9.278-20.682-20.682-20.682h-297.327c-11.404.001-20.681 9.278-20.681 20.682v241.718c0 11.419 9.277 20.709 20.681 20.709h33.801v97.131c0 11.669 9.494 21.163 21.163 21.163h104.223l-8.866 29.646h-14.994c-13.342 0-24.196 10.867-24.196 24.226v11.707c0 6.887 5.603 12.489 12.489 12.489h197.434c6.887 0 12.489-5.603 12.489-12.489v-11.707c0-13.358-10.867-24.226-24.225-24.226h-14.991l-8.847-29.646h71.443v56.735c0 11.763 9.557 21.333 21.305 21.333h91.758c11.763 0 21.333-9.57 21.333-21.333v-151.346c0-11.748-9.57-21.306-21.333-21.306zm-475.667-23.086v-177.438h137.039c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5h-137.039v-49.28c0-3.133 2.548-5.682 5.681-5.682h297.327c3.133 0 5.682 2.549 5.682 5.682v49.279h-137.011c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h137.011v177.438c0 3.147-2.549 5.709-5.682 5.709h-297.327c-3.133.001-5.681-2.56-5.681-5.708zm303.008 20.709c11.404 0 20.682-9.29 20.682-20.709v-112.126h71.7c3.398 0 6.163 2.765 6.163 6.163v129.049h-17.644c-11.748 0-21.305 9.558-21.305 21.306v41.683h-308.122v-65.365h248.526zm21.215 172.166v9.196h-192.411v-9.196c0-5.087 4.125-9.226 9.196-9.226h173.991c5.086 0 9.224 4.139 9.224 9.226zm-79.014-68.872c-4.142 0-7.5 3.357-7.5 7.5s3.358 7.5 7.5 7.5h30.298l8.847 29.646h-112.696l8.866-29.646h30.188c4.142 0 7.5-3.357 7.5-7.5s-3.358-7.5-7.5-7.5h-150.067c-3.398 0-6.163-2.765-6.163-6.163v-16.766h308.123v22.929zm236.791 71.735c0 3.492-2.841 6.333-6.333 6.333h-91.758c-3.477 0-6.305-2.841-6.305-6.333v-151.346c0-3.477 2.828-6.306 6.305-6.306h91.758c3.492 0 6.333 2.829 6.333 6.306z"/><path d="m127.661 124.863h-76.451c-6.949 0-12.603 5.641-12.603 12.574v110.696c0 6.949 5.653 12.603 12.603 12.603h76.451c6.949 0 12.602-5.653 12.602-12.603v-110.695c0-6.934-5.653-12.575-12.602-12.575zm-2.398 120.873h-71.655v-105.873h71.655z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

View File

@@ -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;
}

View File

@@ -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):

View File

@@ -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"
],
)

View File

@@ -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",
},
]

View File

@@ -53,9 +53,30 @@
<div class="grid-row">
{% if navigation_links %}
<div class="tablet:grid-col-2 margin-bottom-4">
{{ sub_navigation(navigation_links) }}
<nav class="nav">
<ul class="usa-sidenav">
{% for item in navigation_links %}
<li class="usa-sidenav__item">
<a href="{{ url_for(item.link) }}"
class="parent-link {% if item['link'] == request.endpoint %} usa-current {% endif %}"
aria-haspopup="true" aria-expanded="false">
{{ item.name }}
</a>
{% if item.sub_navigation_items %}
<ul class="usa-sidenav__sublist" role="menu">
{% for sub_item in item.sub_navigation_items %}
<li role="menuitem">
<a href="{{ url_for(sub_item.link.split('#')[0]) }}#{{ sub_item.link.split('#')[1] }}">{{ sub_item.name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
</div>
<div class="tablet:grid-col-10 padding-left-4 usa-prose site-prose">
<div class="tablet:grid-col-10 tablet:padding-left-4 usa-prose site-prose">
{% else %}
<div class="tablet:grid-col-10">
{% endif %}

View File

@@ -0,0 +1,42 @@
<ul class="usa-card-group">
{% for item in card_contents %}
<li class="usa-card grid-col tablet:grid-col-4 {% if item.link %}linked-card linked-content{% endif %}">
<div class="usa-card__container">
{% if item.card_heading %}
<div class="usa-card__header">
{% if item.link %}
<a href="{{ item.link }}">
{% endif %}
<h3
class="usa-card__heading {% if text_align != 'left' or text_align is not defined %}text-center{% else %}text-left{% endif %} {% if item.link %}linked-card{% endif %}">
{{ item.card_heading }}
</h3>
{% if item.link %}
</a>
{% endif %}
</div>
{% endif %}
{% if item.svg_src or item.image_src%}
<div class="usa-card__media usa-card__media--inset">
{% if item.svg_src %}
<svg aria-hidden="true" focusable="false" role="img" class="best-practices_card_img">
<use xlink:href="{{asset_url('images/best-practices-card-sprite.svg')}}#{{item.svg_src}}"></use>
</svg>
{% endif %}
{% if item.image_src %}
<img class="best-practices_card_img" src="{{item.image_src}}" alt="{{ item.alt_text }}" />
{% endif %}
</div>
{% endif %}
{% if item.p_text %}
<div class="usa-card__body">
<p
class="{% if text_align != 'left' or text_align is not defined %}text-center{% else %}text-left{% endif %} {% if item.link %}linked-card{% endif %}">
{{item.p_text|safe}}
</p>
</div>
{% endif %}
</div>
</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,9 @@
{% macro circle_number(number) %}
<svg xmlns="http://www.w3.org/2000/svg" class="svg-circle-icon" width="24" height="24" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="37" stroke="#005ea2" stroke-width="7" fill="none" />
<text x="50%" y="50%" text-anchor="middle" font-size="40" font-weight="bolder" fill="#005ea2" font-family="Arial"
dy=".35em">
{{ number }}
</text>
</svg>
{% endmacro %}

View File

@@ -0,0 +1,13 @@
<nav class="navigation-service usa-breadcrumb padding-top-0">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<span class="usa-breadcrumb__label"><a href="{{ url_for('.best_practices') }}"
class="usa-link navigation-organization-link">Best Practices</a></span>
</li>
{% if title %}
<li class="usa-breadcrumb__list-item">
<span class="usa-breadcrumb__label">{{ title }}</span>
</li>
{% endif %}
</ol>
</nav>

View File

@@ -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 %}
<section class="usa-prose">
<h1>{{page_title}}</h1>
<p class="font-sans-lg text-base">Learn how effective your texting program can be.</p>
<p>
Several factors determine a messaging campaigns 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.
</p>
<h2>What other texting studies have found</h2>
<p>
When the <a
href="https://www.cbpp.org/research/food-assistance/targeted-text-message-outreach-can-increase-wic-enrollment-pilots-show"
target="_blank">Center on Budget and Policy Priorities</a> studied WIC, they found key learnings about the
quantity of messages delivered, how people engage with messages, and how they take action.
</p>
<h3 class="padding-left-5">Message delivery</h3>
<p class="font-body-lg text-light padding-left-5">Benchmark: <span class="text-bold">80%</span> of texts are
successfully delivered
</p>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>
You may discover that some numbers are temporarily or permanently unavailable due to service being
discontinued,
numbers changing, or being a landline.
</p>
</li>
</ul>
</div>
<h3 class="padding-left-5">Engagement</h3>
<div class="measure-4 padding-left-5">
<p class="font-body-lg text-light">Benchmark: Engagement rates with texts ranged from <span
class="text-bold">17%</span>
to <span class="text-bold">26%</span></p>
</div>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>
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.
</p>
</li>
</ul>
</div>
<h3 class="padding-left-5">Appointment requests</h3>
<div class="measure-4 padding-left-5">
<p class="font-body-lg text-light">Benchmark: Requesting appointments after receiving texts ranged
from <span class="text-bold">4%</span> to <span class="text-bold">9%</span></p>
</div>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>
Requesting appointments is a specific type of engagement. Provide a phone number or link to an online
appointment
request form.
</p>
</li>
</ul>
</div>
<p>
The <a href="https://codeforamerica.org/resources/texting-playbook/" target="_blank">Code for Americas Texting
Playbook</a>
reported specific learnings around appointment reminders, completing
document submission, and maintenance reminders.
</p>
<h3 class="padding-left-5">Appointment reminders</h3>
<div class="measure-4 padding-left-5">
<p class="font-body-lg text-light">Benchmark: Clients were <span class="text-bold">79%</span> more
likely
to keep
their appointment after receiving a text reminder.</p>
</div>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>You will likely see more completed appointments.</p>
</li>
</ul>
</div>
<div class="measure-4 padding-left-5">
<p class="font-body-lg text-light">Benchmark: Clients were <span class="text-bold">55%</span> more
likely
to complete
an interview after receiving an interview reminder</p>
</div>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>You will likely see more completed interviews.</p>
</li>
</ul>
</div>
<h3 class="padding-left-5">Document submission</h3>
<div class="measure-4 padding-left-5">
<p class="font-body-lg text-light">
Benchmark: Clients were <span class="text-bold">6%</span> more likely to complete document submission after
receiving a customized list of required documents via text
</p>
</div>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>To encourage response, provide a custom list of the needed documents and information about how to submit
them.
</p>
</li>
</ul>
</div>
<h3 class="padding-left-5">Reminders</h3>
<div class="measure-4 padding-left-5">
<p class="font-body-lg text-light">Benchmark: Text reminders improved case maintenance rates by <span
class="text-bold">21%</span></p>
</div>
<div class="padding-left-2 measure-4">
<ul>
<li>
<p>You may see less turnover in your case rates.</p>
</li>
</ul>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block per_page_title %}
Best Practices
{% endblock %}
{% block content_column_content %}
<section class="usa-prose">
<h1>Best Practices</h1>
<p class="font-sans-lg text-base">For texting the public</p>
<p>Effectively reaching your audience and supporting your programs goals starts with strategically planning out what
text messages can help you achieve and how to approach a thoughtful rollout.
</p>
<p>This set of best practices will help you get an effective texting initiative up and running.</p>
<h2 class="padding-bottom-2">
Key elements of a texting campaign
</h2>
{% 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 %}
</section>
{% endblock %}

View File

@@ -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 %}
<section class="usa-prose">
<h1>{{page_title}}</h1>
<p class="font-sans-lg text-base">Start with a singular purpose. Make explicit what you want to achieve.</p>
<div class="grid-row grid-gap-3">
<div class="tablet:grid-col">
<p>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, its important to start
with
the end in mind.
</p>
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
<div class="usa-summary-box__body">
<p class="usa-summary-box__heading" id="summary-box-establish-clear-goals">
To establish clear goals, start by answering the following questions:
</p>
<div class="usa-summary-box__text">
<ul class="usa-list">
<li><i>Who</i> do you want to text?</li>
<li><i>At what moments</i> do you want to text them?</li>
<li><i>Why</i> do you want to text them?</li>
<li>What do you want them to <i>do</i>?</li>
</ul>
</div>
</div>
</div>
</div>
<div class="tablet:grid-col width-full padding-top-3">
<div class="grid-container">
<div class="usa-card__container border-0 width-full overflow-hidden maxh-mobile-lg">
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
<div class="position-absolute flex text-center padding-5 padding-top-15">
<p class="sms-message-wrapper text-left">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.
</p>
</div>
</div>
</div>
</div>
</div>
<hr>
<h2>When should you send a text?</h2>
<div class="grid-row grid-gap-3">
<div class="tablet:grid-col">
<div class="do-dont">
<div class="do-dont__do">
<h3 class="do-dont__heading"><a id="what-to-do" class="usa-anchor"></a>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ url_for('static', filename='img/sprite.svg') }}#check_circle"></use>
</svg>
What to do
</h3>
<div class="do-dont__content">
<p><b>Use texting to</b> share timely
information, provide critical status updates, or remind someone to do something.</p>
<p class="margin-bottom-2">Examples:</p>
{% 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…",
},
] %}
<ul>
{% for item in list_item_dos %}
<li>
{{item.list_content}}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
<div class="tablet:grid-col">
<div class="do-dont">
<div class="do-dont__dont">
<h3 class="do-dont__heading"><a id="what-not-to-do" class="usa-anchor"></a>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{{ url_for('static', filename='img/sprite.svg') }}#cancel"></use>
</svg> What not to do
</h3>
<div class="do-dont__content">
<p>Texting is <b>not great for</b> long,
detailed instructions or frequent reminders that may be ignored.
</p>
<p class="margin-bottom-2">Examples:</p>
{% 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",
},
] %}
<ul>
{% for item in list_item_donts %}
<li>
{{item.list_content}}
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</div>
<p>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?
</p>
<p>
The Department of Veterans Affairs provides a <a class="use-link usa-link--external"
href="https://github.com/department-of-veterans-affairs/va.gov-team/blob/master/products/va-notify/notification-guide.md#vanotify-notification-guide"
target="_blank">helpful flow-chart</a> that can help you decide if a text message is needed
for the communication problem you are trying to solve.
</p>
<h2>Use a hypothesis framework to plan your campaign</h2>
<p>
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:
</p>
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
<div class="usa-summary-box__body">
<div class="usa-summary-box__text">
<p class="margin-top-0 font-body-lg">
We believe that texting <i>[specific group of people]</i> when <i>[this happens or on this
cadence]</i> to inform/alert them about <i>[subject]</i> will result in <i>[desired action]</i>.
</p>
</div>
</div>
</div>
<p>
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 <a href="../best-practices/write-for-action">write text message
content
that provokes action</a>.
</p>
<h2>Begin with a small test</h2>
<p>
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.
</p>
<p>
Starting small has less risk and more flexibility. It can help you gather data and decide <i>if and how</i> to send
more text messages. For example, start by sending one message to a smaller sample of your audience and track their
resulting behavior.
</p>
<p>
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.
</p>
<h2>Measure performance</h2>
<p>Building and operationalizing a texting initiative requires time and effort, and you want to be able to demonstrate
the results of that effort.</p>
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
<div class="usa-summary-box__body">
<div class="usa-summary-box__text">
<p class="margin-top-0">
To understand if your texting program is successful, ask:
</p>
<ul class="usa-list">
<li>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?</li>
<li>How will you know if text recipients take the desired action?</li>
<li>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?</li>
</ul>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -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 %}
<section class="usa-prose">
<h1>{{page_title}}</h1>
<p class="font-sans-lg text-base">Help your audience anticipate and welcome your texts.</p>
<p>
People are wary of texts theyre 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.
</p>
<h2 id="get-the-word-out">Get the word out</h2>
<p>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.</p>
<p>How you currently communicate with your audience will determine the best approaches to announce your new
initiative. Harness channels youre 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.</p>
<h3>An example of key messages in direct mail</h3>
<figure>
<img src="{{asset_url('images/snap-renewal.jpg')}}" alt="snap-renewal" width="100%">
<figcaption>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.</figcaption>
</figure>
<h3>Prime your audiences to trust you</h3>
<ul class="usa-card-group flex-1 flex-wrap padding-top-2">
<li class="flex-1 width-full height-full padding-bottom-2">
<div class="usa-card__container border-0">
<p>To reinforce legitimacy, include these key messages in your outreach:</p>
<ol class="best-practices-list">
<li><b>Introduce yourself</b> Give people time to get to know your texting communications before you need
direct action from them</li>
<li><b>Phone number</b> Note the sending phone number your texts will come from</li>
<li><b>Text types</b> Be clear on the types of texts you intend to send, how often, and why they matter</li>
<li><b>Privacy policy</b> For example, state that you will not collect personal information, including the
types of information you will never ask for</li>
<li><b>Places to verify</b> Include a phone number, website, or human where people can verify that the texts
are coming from you</li>
<li><b>Opt out</b> Provide opt-out instructions and a disclaimer for the potential cost of receiving the
text
</li>
</ol>
</div>
</li>
<li class="usa-card flex-1 usa-card--media-right width-full padding-top-3">
<div class="usa-card__container border-0 width-full overflow-hidden height-phone-lg">
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
<div class="position-absolute flex text-center padding-5 padding-top-15">
<p class="sms-message-wrapper text-left">Dept. of Social Services: Welcome to text updates from the
{{circle_number(1)}}<b>Department of Social Services.</b> {{ circle_number(2) }}<b>Well always text
you from
this number,</b> save us to your contact list. Our goal is to {{ circle_number(3) }} <b>keep you up to
date on
your account status.</b> {{ circle_number(4) }} <b>We will never ask for personal details in a text.</b>
{{ circle_number(5) }} If you have questions about how we protect your privacy, see
state.gov/privacy.{{circle_number(6) }}<b>Reply STOP to stop.</b>
</p>
</div>
</div>
</li>
</ul>
<hr>
<h2 id="as-people-receive-texts">As people receive texts</h2>
<p>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, its important to
repeat consistent and standardized language across texts and programs.</p>
<h3>Be trustworthy</h3>
<ul class="usa-card-group flex-1 flex-wrap padding-top-2">
<li class="flex-1 width-full height-full padding-bottom-2">
<div class="usa-card__container border-0">
<ol start="7" class="best-practices-list set-two">
<li>Identify yourself in the text message. Use a consistent and familiar <b>program name</b> to precede each
text you send.</li>
<li>Personalize with <b>first name</b>. Through A/B testing, Code for America saw an increase in
follow-through by 25% if someones first name was included.</li>
<li>Use <b>full and direct .gov website URLs</b>. Avoid link shorteners, as they obscure your links and
potentially make it easier for a malicious actor to spoof your text.</li>
<li>If providing a phone number, use a familiar phone number and <b>provide information on how to verify</b>
its legitimacy.</li>
</ol>
</div>
</li>
<li class="usa-card flex-1 usa-card--media-right width-full padding-top-1">
<div class="usa-card__container border-0 width-full height-mobile overflow-hidden">
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
<div class="position-absolute flex text-center padding-5 padding-top-10">
<p class="sms-message-wrapper text-left">
{{circle_number(7) }}<b>Dept. of Social Services:</b> Hi {{circle_number(8) }}<b>Julie,</b> Your Medicaid
renewal is closing December 31, 2023. You can renew online at {{circle_number(9) }}<a
href="https://www.application.yourstate.gov" target="_blank"
rel="application.yourstate.gov">https://www.application.yourstate.gov</a> or {{circle_number(10) }}
<b>call the number on the back</b> of your Medicaid card.
</p>
</div>
</div>
</li>
</ul>
<h3>Provide helpful auto-responses</h3>
<ul class="usa-card-group flex-1 flex-wrap padding-top-2">
<li class="flex-1 width-full height-full padding-bottom-2">
<div class="usa-card__container border-0">
<p>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.</p>
<ol class="best-practices-list set-three">
<li>Provide <b>a way to contact a human</b> in any auto-response text</li>
<li>Provide <b>information on how to opt out</b> and opt back in</li>
</ol>
</div>
</li>
<li class="usa-card flex-1 usa-card--media-right width-full padding-top-3">
<div class="usa-card__container border-0 width-full overflow-hidden best-practices">
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
<div class="position-absolute flex text-center padding-5 padding-top-15">
<p class="sms-message-wrapper text-left">
Dept. of Social Services: This number is unmonitored. To {{circle_number(11) }} <b>contact us, call
888-123-4567</b>. 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) }} <b>Reply “STOP”</b> to this phone number
to opt out of receiving texts, and “START” to opt back in.
</p>
</div>
</div>
</li>
</ul>
<h2>Be prepared for questions</h2>
<p>
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 youre priming your
delivery staff, especially those who interact directly with your intended audience.
</p>
<h3 class="padding-bottom-2">Harness trusted communication channels</h3>
{% 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 %}
<h3 class="padding-bottom-2">Prepare your team</h3>
{% 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
youre sending, when youll 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 %}
</section>
{% endblock %}

View File

@@ -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 %}
<section class="usa-prose">
<h1>{{page_title}}</h1>
<p class="font-sans-lg text-base"> What to know as you plan translated texts.</p>
<p>
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.
</p>
<p>
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.
</p>
<p>
As with messages in English, we strongly recommend user testing the translations with people who speak the language
to ensure the messages are understandable.
</p>
</section>
{% endblock %}

View File

@@ -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 %}
<section class="usa-prose">
<h1>{{page_title}}</h1>
<p class="font-sans-lg text-base">Understand what is required when texting the public.</p>
<h2>What to know about consent and opting out</h2>
<p>
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.
</p>
<p>
If you do need expressed consent, consider including a pre-checked plain language opt-in (i.e. “Its OK to text
me.”) on
digital forms. Be sure to ask for an up-to-date phone number and include a question about the recipients preferred
language for text messages if you expect to <a href="../best-practices/multiple-languages">translate your text
messages</a> in
languages other than English.
</p>
{% set links = [
{
"p_text": 'The <a class="use-link usa-link--external" href="https://www.fcc.gov/sites/default/files/tcpa-rules.pdf"
target="_blank">Telephone Consumer
Protection Act (TCPA)</a> (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 <a class="use-link usa-link--external"
href="https://www.fcc.gov/document/fcc-provides-best-practices-enable-critical-health-care-coverage-calls"
target="_blank">FCC has ruled</a> that <b>Federal</b> and <b>State</b> 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 class="use-link usa-link--external"
href="https://www.performance.gov/cx/life-experiences/facing-a-financial-shock/outputs/fcc-training/"
target="_blank">a recorded training on public
benefits texting</a> provided by the FCC.'
},
] %}
<h3>Complying with the Telephone Consumer Protection Act (TCPA)</h3>
<ul class="usa-collection">
{% for item in links %}
<li class="usa-collection__item">
<div class="usa-collection__body">
{% if item.url_text %}
<h4 class="usa-collection__heading">
<a class="usa-link {% if item.is_downloadable %}usa-link--downloadable{% else %}usa-link--external{% endif %}"
href="{% if item.is_downloadable %}{{ asset_url(item.url_link) }}{% else %}{{ item.url_link }}{% endif %}"
target="_blank">
{{ item.url_text }}
</a>
</h4>
{% endif %}
<p class="usa-collection__description">
{{item.p_text |safe}}
</p>
</div>
</li>
{% endfor %}
</ul>
<h3>Opting out</h3>
<p>
There is no policy requirement for senders to communicate opt-out options, but <a
href="../best-practices/establish-trust#as-people-receive-texts"> including instructions in introductory and/or
auto-response texts </a> on how to opt out and opt back in are effective ways to establish trust with your
audience.
</p>
<p>
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 <a class="usa-link usa-link--external"
href="https://docs.aws.amazon.com/pinpoint/latest/userguide/channels-sms-limitations-opt-out.html#settings-sms-self-opt-out-terms"
target="_blank">one of several other keywords</a>.
</p>
<p>
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.
</p>
<h2>Honoring privacy</h2>
<h3>Phone numbers are personally identifiable information (PII)</h3>
<p>
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.
</p>
<h3>Dont text sensitive information</h3>
<p>
Sensitive information should be protected and secure. Social Security Numbers, drivers 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.
</p>
<h3>Assess risk</h3>
<p>
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 <a class="use-link usa-link--external"
href="https://www.fcc.gov/reassigned-numbers-database" target="_blank"
rel="Reassigned Numbers Database">Reassigned
Numbers Database</a> to identify phone numbers that may have been reassigned since your agency obtained them.
</p>
</section>
{% endblock %}

View File

@@ -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 %}
<section class="usa-prose">
<h1>{{page_title}}</h1>
<p class="font-sans-lg text-base"> Help your audience know what to do with the information you send.</p>
<p>
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.
</p>
<div class="usa-summary-box maxw-tablet" role="region" aria-labelledby="summary-box-establish-clear-goals">
<div class="usa-summary-box__body">
<p class="usa-summary-box__heading" id="summary-box-establish-clear-goals">
When deciding what types of messages to draft for your campaign, think through the following questions:
</p>
<div class="usa-summary-box__text">
<ul class="usa-list">
<li>What do you want recipients to do when they receive the text?</li>
<li>What will happen if a recipient doesn't take the action you want them to take?</li>
<li>Will you send any follow-up texts depending on if the action was taken or not?</li>
</ul>
</div>
</div>
</div>
<h2>Be clear and concise</h2>
<ul>
<li>Use simple, everyday language. Keep the text short and clear. Stay under 300 characters.</li>
<li>
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.
</li>
<li>Use a neutral, direct, and professional tone. This works better than a friendly or overly casual tone.</li>
</ul>
<h2>Build conditions for action</h2>
<ul class="usa-card-group flex-1 flex-wrap padding-top-2 best-practices-flex-container">
<li class="flex-1 width-full height-full padding-bottom-2">
<div class="usa-card__container border-0">
<p>For example, getting a person to update their mailing address:</p>
<ol class="best-practices-list">
<li>Clearly state the information and the response you want the recipient to take.</li>
<li>Point directly to where the action can be completed, not to more information.</li>
<li>Make sure the action can be completed via mobile phone, like calling a person or going to a
mobile-friendly URL.
</li>
<li>
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.
</li>
<li>Consider clearly explaining any consequences if the desired action is not completed.</li>
</ol>
</div>
</li>
<li class="flex-1 usa-card--media-right width-full padding-top-3">
<div class="usa-card__container border-0 width-full">
<img class="width-full" src="{{ asset_url('images/phone_grey.svg') }}" role="img" alt="Phone" />
<div class="position-absolute flex text-center padding-5 padding-top-15">
<p class="sms-message-wrapper text-left">{{circle_number(1) }}State Medicare Agency: Hello, MEDICARE has
attempted to
send you an update about your claim, but the mail was returned. <br>{{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.
</p>
</div>
</div>
</li>
</ul>
<h2>What provoking action looks like</h2>
<p class="padding-bottom-2"><a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC10002044/" target="_blank">Evidence
shows</a> 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:</p>
{% set card_contents = [
{
"card_heading": "Feeling a sense of ownership can influence behavior.",
"p_text": "Example <b>Don't miss this chance to get free internet</b> or $30 per month off your internet bill.",
},
{
"card_heading": "Individuals are strongly influenced by others behaviors.",
"p_text": "Example <b>Thousands of residents</b> 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 <b>that belong to you</b>.",
},
] %}
{% with card_contents=card_contents, text_align='left' %}{% include "components/best-practices/cards.html" %}{%
endwith %}
</section>
{% endblock %}

View File

@@ -16,17 +16,22 @@
<p>Below we explain how to:</p>
<ul class="list list-bullet">
{% if not feature_best_practices_enabled %}
<li><a class="usa-link" href="#format-content">Format your content</a></li>
<li><a class="usa-link" href="#add-links">Add links</a></li>
{% endif %}
<li><a class="usa-link" href="#personalize-content">Personalize your content</a></li>
<li><a class="usa-link" href="#conditional-content">Add conditional content</a></li>
<li><a class="usa-link" href="#indentify-program">Identify your program</a></li>
<li><a class="usa-link" href="#prepare-data">Prepare your data</a></li>
{% if not feature_best_practices_enabled %}
<li><a class="usa-link" href="#prevent-fraud">Prevent fraud</a></li>
{% endif%}
</ul>
{# Format content #}
<h2 class="padding-top-1" id="format-content">Format your content</h2>
{% if not feature_best_practices_enabled %}
<p>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:</p>
@@ -37,7 +42,7 @@
<li>Say only one important thing per message.</li>
<li>Frame the message to encourage action and explain the consequences of not completing the desired action.</li>
</ul>
{% endif %}
<h3>To create and format your message</h3>
<ol class="list">
<li>All messages start from a template</li>
@@ -45,6 +50,7 @@
<li>Add a new template or choose an existing template and select Edit.</li>
</ol>
{% if not feature_best_practices_enabled %}
{# Add links #}
<h2 class="padding-top-1" id="add-links">Add links</h2>
<p>When composing a text message, links to websites or online applications can help your recipient respond quickly.</p>
@@ -64,6 +70,7 @@
<li>Your link might stop working if theres a service outage.</li>
<li>You can no longer control where the redirect goes.</li>
</ul>
{% endif %}
{# Personalize content #}
<h2 class="padding-top-1" id="personalize-content">Personalize your content</h2>
@@ -214,6 +221,7 @@
</table>
</div>
{% if not feature_best_practices_enabled %}
{# Preventing fraud #}
<h2 class="padding-top-1" id="prevent-fraud">Preventing fraud</h2>
<p>Texting fraud is ever prevalent and while we cant 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.</p>
</li>
</ol>
{% endif %}
{% endblock %}

View File

@@ -33,7 +33,7 @@
<ul class="list list-bullet">
<li>Achieve compliance to begin piloting, such as ATO and privacy standards.
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
use xlink:href="/assets/img/sprite.svg#check"></use>
<use xlink:href="/assets/img/sprite.svg#check"></use>
</svg>
</li>
<li>Demonstrate that a government-run notifications tool provides a unique value.</li>

View File

@@ -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) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 908 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 558 KiB

After

Width:  |  Height:  |  Size: 558 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

56
docs/feature-flagging.md Normal file
View File

@@ -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 <APP_NAME> <ENV_VAR_NAME> <VALUE>
```
#### Example:
```
cf set-env notify-admin-sandbox FEATURE_BEST_PRACTICES_ENABLED true
```
### 4. Restage the Application
```
cf restage <APP_NAME>
```
#### 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.

View File

@@ -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;

View File

@@ -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))

View File

@@ -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 dont have a recent service
with client_request.session_transaction() as session:
session["service_id"] = None
request()
# Check it still works when they dont 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 services 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

View File

@@ -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",
},
)
)

View File

@@ -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
];