Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Enrollment index #1502

Merged
merged 7 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions benefits/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2.3 on 2023-07-06 23:24
# Generated by Django 4.2.3 on 2023-07-08 02:54

from django.db import migrations, models
import django.db.models.deletion
Expand Down Expand Up @@ -60,8 +60,6 @@ class Migration(migrations.Migration):
("form_name_max_length", models.PositiveSmallIntegerField(null=True)),
("unverified_title", models.TextField()),
("unverified_blurb", models.TextField()),
("eligibility_confirmed_item_heading", models.TextField(null=True)),
("eligibility_confirmed_item_details", models.TextField(null=True)),
("enrollment_success_confirm_item_details", models.TextField()),
("enrollment_success_expiry_item_heading", models.TextField(null=True)),
("enrollment_success_expiry_item_details", models.TextField(null=True)),
Expand Down
10 changes: 0 additions & 10 deletions benefits/core/migrations/0002_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,6 @@ def load_data(app, *args, **kwargs):
start_template="eligibility/start__login_gov.html",
unverified_title=_("eligibility.pages.unverified.title"),
unverified_blurb=_("eligibility.pages.unverified.p[0]"),
eligibility_confirmed_item_heading=_("enrollment.pages.index.login_gov.eligibility_confirmed_item.heading"),
eligibility_confirmed_item_details=_(
"enrollment.pages.index.login_gov.eligibility_confirmed_item.details%(transit_agency_short_name)s"
),
enrollment_success_confirm_item_details=_("enrollment.pages.success.login_gov.confirm_item.details"),
enrollment_success_expiry_item_heading=None,
enrollment_success_expiry_item_details=None,
Expand Down Expand Up @@ -205,8 +201,6 @@ def load_data(app, *args, **kwargs):
form_name_max_length=255,
unverified_title=_("eligibility.pages.unverified.mst_cc.title"),
unverified_blurb=_("eligibility.pages.unverified.mst_cc.p[0]"),
eligibility_confirmed_item_heading=None,
eligibility_confirmed_item_details=None,
enrollment_success_confirm_item_details=_("enrollment.pages.success.mst_cc.confirm_item.details"),
enrollment_success_expiry_item_heading=_("enrollment.pages.success.mst_cc.expiry_item.heading"),
enrollment_success_expiry_item_details=_("enrollment.pages.success.mst_cc.expiry_item.details"),
Expand All @@ -220,10 +214,6 @@ def load_data(app, *args, **kwargs):
start_template="eligibility/start__login_gov.html",
unverified_title=_("eligibility.pages.unverified.title"),
unverified_blurb=_("eligibility.pages.unverified.p[0]"),
eligibility_confirmed_item_heading=_("enrollment.pages.index.login_gov.eligibility_confirmed_item.heading"),
eligibility_confirmed_item_details=_(
"enrollment.pages.index.login_gov.eligibility_confirmed_item.details%(transit_agency_short_name)s"
),
enrollment_success_confirm_item_details=_("enrollment.pages.success.login_gov.confirm_item.details"),
enrollment_success_expiry_item_heading=None,
enrollment_success_expiry_item_details=None,
Expand Down
2 changes: 0 additions & 2 deletions benefits/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,6 @@ class EligibilityVerifier(models.Model):
form_name_max_length = models.PositiveSmallIntegerField(null=True)
unverified_title = models.TextField()
unverified_blurb = models.TextField()
eligibility_confirmed_item_heading = models.TextField(null=True)
eligibility_confirmed_item_details = models.TextField(null=True)
# Fields for the dynamic enrollment success message
enrollment_success_confirm_item_details = models.TextField()
enrollment_success_expiry_item_heading = models.TextField(null=True)
Expand Down
27 changes: 0 additions & 27 deletions benefits/core/viewmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,33 +242,6 @@ def not_found(
return ErrorPage(title=title, headline=headline, paragraphs=paragraphs, **kwargs)


class PaymentProcessor:
"""
Represents a core.models.PaymentProcessor:
* model: core.models.PaymentProcessor
* access_token_url: str
* element_id: str
* color: str
* [name: str]
* [loading_text: str]
"""

def __init__(self, model, access_token_url, element_id, color, name=None, loading_text=_("core.buttons.wait")):
if isinstance(model, models.PaymentProcessor):
self.access_token_url = access_token_url
self.element_id = element_id
self.color = color
self.name = name or model.name
self.loading_text = loading_text
self.card_tokenize_url = model.card_tokenize_url
self.card_tokenize_func = model.card_tokenize_func
self.card_tokenize_env = model.card_tokenize_env

def context_dict(self):
"""Return a context dict for a PaymentProcessor."""
return {"payment_processor": self}


class TransitAgency:
"""
Represents a core.models.TransitAgency:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "core/includes/media_item.html" %}
{% load i18n %}

{% block icon %}
{% translate "core.icons.bankcardcheck" context "image alt text" as icon_alt %}
{% include "core/includes/icon__direct_args.html" with name="bankcardcheck" alt=icon_alt %}
{% endblock icon %}

{% block heading %}
{% translate "enrollment.pages.index.link_card_item.heading" %}
{% endblock heading %}

{% block body %}
<div class="media-body--details">
<p>
{% translate "enrollment.pages.index.link_card_item.p[0]s[0]" %} <a href="#">Littlepay</a>.
{% translate "enrollment.pages.index.link_card_item.p[0]s[1]" %}
</p>
<p>{% translate "enrollment.pages.index.link_card_item.p[1]s[0]" %}</p>
</div>
{% endblock body %}
167 changes: 86 additions & 81 deletions benefits/enrollment/templates/enrollment/index.html
Original file line number Diff line number Diff line change
@@ -1,108 +1,113 @@
{% extends "core/base.html" %}
{% load i18n %}

{% block page_title %}
{% translate "enrollment.pages.index.title" %}&nbsp;|&nbsp;
{% endblock page_title %}

{% block nav-buttons %}
{% include "core/includes/sign-out-link.html" %}
{% endblock nav-buttons %}

{% block headline %}
<div class="col-lg-6">
<h1 class="pb-lg-8 pb-4">{{ page.headline }}</h1>
<h1 class="pb-lg-8 pb-4">{% translate "enrollment.pages.index.headline" %}</h1>
</div>
{% endblock headline %}

{% block inner-content %}
<div class="col-12 col-sm-12 col-lg-10">{% include "core/includes/media-list.html" with media=media %}</div>

{% comment %} This Javascript code must be before the forms. {% endcomment %}
{% if payment_processor %}
<script nonce="{{ request.csp_nonce }}">
var startedEvent = "started payment connection", closedEvent = "closed payment connection";
<div class="col-12 col-sm-12 col-lg-10">
<ul class="media-list mx-0 px-0 d-flex justify-content-center flex-column">
{% include "enrollment/includes/media-item--bankcardcheck--index.html" %}
</ul>
</div>
{% comment %}
This Javascript code is partially generated by this template and so it must
come before the forms, which are rendered at just before the {% endblock inner-content %}
{% endcomment %}
{% url "enrollment:token" as access_token_url %}
{% translate "core.buttons.wait" as loading_text %}
thekaveman marked this conversation as resolved.
Show resolved Hide resolved
<script nonce="{{ request.csp_nonce }}">
var startedEvent = "started payment connection", closedEvent = "closed payment connection";

$.ajax({ dataType: "script", attrs: { nonce: "{{ request.csp_nonce }}"}, url: "{{ payment_processor.card_tokenize_url }}" })
.done(function() {
$.get("{{ payment_processor.access_token_url }}", function(data) {
$(".loading").remove();
$(".invisible").removeClass("invisible");
$.ajax({ dataType: "script", attrs: { nonce: "{{ request.csp_nonce }}"}, url: "{{ card_tokenize_url }}" })
.done(function() {
$.get("{{ access_token_url }}", function(data) {
$(".loading").remove();
$(".invisible").removeClass("invisible");

$("{{ payment_processor.element_id }}").on("click", function() {
amplitude.getInstance().logEvent(startedEvent, {
card_tokenize_url: "{{ payment_processor.card_tokenize_url }}",
card_tokenize_func: "{{ payment_processor.card_tokenize_func }}"
});
$(this).addClass("disabled").attr("aria-disabled", "true").text("{{ payment_processor.loading_text }}");
});
$("#{{ cta_button }}").on("click", function() {
amplitude.getInstance().logEvent(startedEvent, {
card_tokenize_url: "{{ card_tokenize_url }}",
card_tokenize_func: "{{ card_tokenize_func }}"
});
$(this).addClass("disabled").attr("aria-disabled", "true").text("{{ loading_text }}");
});

{{ payment_processor.card_tokenize_func }}({
authorization: data.token,
element: '{{ payment_processor.element_id }}',
envUrl: '{{ payment_processor.card_tokenize_env }}',
options: {
name: '{{ payment_processor.name }}',
color: '{{ payment_processor.color }}'
},
onTokenize: function (response) {
/* This function executes when the
/* card/address verification returns
/* successfully with a token from enrollment server */
amplitude.getInstance().logEvent(closedEvent, {status: "success"});
{{ card_tokenize_func }}({
authorization: data.token,
element: '#{{ cta_button }}',
envUrl: '{{ card_tokenize_env }}',
options: {
color: '#046b99'
},
onTokenize: function (response) {
/* This function executes when the
/* card/address verification returns
/* successfully with a token from enrollment server */
amplitude.getInstance().logEvent(closedEvent, {status: "success"});

var form = $("form[action='{{ forms.tokenize_success }}']");
$("#card_token", form).val(response.token);
form.submit();
},
onVerificationFailure: function (response) {
/* This function executes when the
/* card/address verification fails and server
/* return verification failure message */
amplitude.getInstance().logEvent(closedEvent, {status: "fail"});
var form = $("form#{{ form_success }}");
$("#{{ token_field }}", form).val(response.token);
form.submit();
},
onVerificationFailure: function (response) {
/* This function executes when the
/* card/address verification fails and server
/* return verification failure message */
amplitude.getInstance().logEvent(closedEvent, {status: "fail"});

var form = $("form[action='{{ forms.tokenize_retry }}']");
form.submit();
},
onError: function (response) {
/* This function executes when the
/* server returns error or token is invalid.
/* 400 or 500 will return. */
amplitude.getInstance().logEvent(closedEvent, {status: "error", error: response});
var form = $("form#{{ form_retry }}");
form.submit();
},
onError: function (response) {
/* This function executes when the
/* server returns error or token is invalid.
/* 400 or 500 will return. */
amplitude.getInstance().logEvent(closedEvent, {status: "error", error: response});

var form = $("form[action='{{ forms.tokenize_retry }}']");
form.submit();
},
onCancel: function () {
/* This function executes when the
/* user cancels and closes the window
/* and returns to home page. */
amplitude.getInstance().logEvent(closedEvent, {status: "cancel"});
var form = $("form#{{ form_retry }}");
form.submit();
},
onCancel: function () {
/* This function executes when the
/* user cancels and closes the window
/* and returns to home page. */
amplitude.getInstance().logEvent(closedEvent, {status: "cancel"});

return location.reload();
}
});
});
})
.fail(function(jqxhr, settings, exception) {
$(".loading").remove();
console.log(exception);
});
</script>
{% else %}
<h2>Error!</h2>
<p>No access token configured</p>
{% endif %}
return location.reload();
}
});
});
})
.fail(function(jqxhr, settings, exception) {
$(".loading").remove();
console.log(exception);
});
</script>

{% for f in page.forms %}
{% for f in forms %}
{% include "core/includes/form.html" with form=f %}
{% endfor %}
{% endblock inner-content %}

{% block call-to-action-button %}
{% if payment_processor %}
<button type="button" class="btn btn-primary btn-lg loading" role="status" disabled>
{{ payment_processor.loading_text }}
<button type="button" class="btn btn-primary btn-lg loading" role="status" disabled>
{% translate "core.buttons.wait" %}
</button>
<div class="invisible">
<button id="{{ cta_button }}" href="#{{ cta_button }}" class="btn btn-lg btn-primary" role="button">
{% translate "enrollment.buttons.payment_partner" %}
</button>
<div class="invisible">
{% for b in page.buttons %}
{% include "core/includes/button.html" with button=b %}
{% endfor %}
</div>
{% endif %}
</div>
{% endblock call-to-action-button %}
74 changes: 12 additions & 62 deletions benefits/enrollment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def index(request):
session.update(request, origin=reverse(ROUTE_INDEX))

agency = session.agency(request)
verifier = session.verifier(request)

# POST back after payment processor form, process card token
if request.method == "POST":
Expand All @@ -70,73 +69,24 @@ def index(request):
analytics.returned_error(request, response.message)
raise Exception(response.message)

# GET enrollment index, with button to initiate payment processor connection
# GET enrollment index
else:
tokenize_button = "tokenize_card"
tokenize_retry_form = forms.CardTokenizeFailForm(ROUTE_RETRY)
tokenize_success_form = forms.CardTokenizeSuccessForm(auto_id=True, label_suffix="")

media = []

if verifier.eligibility_confirmed_item_heading or verifier.eligibility_confirmed_item_details:
heading = _(verifier.eligibility_confirmed_item_heading) if verifier.eligibility_confirmed_item_heading else None
details = (
_(verifier.eligibility_confirmed_item_details) % {"transit_agency_short_name": agency.short_name}
if verifier.eligibility_confirmed_item_details
else None
)
confirmed_eligibility_item = viewmodels.MediaItem(
icon=viewmodels.Icon("happybus", pgettext("image alt text", "core.icons.happybus")),
heading=heading,
details=details,
)
media.append(confirmed_eligibility_item)

help_link = reverse(ROUTE_HELP)
link_card_item = viewmodels.MediaItem(
icon=viewmodels.Icon("bankcardcheck", pgettext("image alt text", "core.icons.bankcardcheck")),
heading=_("enrollment.pages.index.link_card_item.heading"),
details=[
format_html(_("enrollment.pages.index.link_card_item.p[0]%(link)s") % {"link": f"{help_link}#littlepay"}),
_("enrollment.pages.index.link_card_item.p[1]"),
],
)
media.append(link_card_item)

page = viewmodels.Page(
title=_("enrollment.pages.index.title"),
headline=_("enrollment.pages.index.headline"),
forms=[tokenize_retry_form, tokenize_success_form],
buttons=[
viewmodels.Button.primary(
text=_("enrollment.buttons.payment_partner"), id=tokenize_button, url=f"#{tokenize_button}"
),
],
)
context = {"media": media}
context.update(page.context_dict())

# add agency details
agency_vm = viewmodels.TransitAgency(agency)
context.update(agency_vm.context_dict())

# and payment processor details
processor_vm = viewmodels.PaymentProcessor(
model=agency.payment_processor,
access_token_url=reverse(ROUTE_TOKEN),
element_id=f"#{tokenize_button}",
color="#046b99",
name=f"{agency.long_name} {_('partnered with')} {agency.payment_processor.name}",
)
context.update(processor_vm.context_dict())
logger.warning(f"card_tokenize_url: {context['payment_processor'].card_tokenize_url}")

# the tokenize form URLs are injected to page-generated Javascript
context["forms"] = {
"tokenize_retry": reverse(tokenize_retry_form.action_url),
"tokenize_success": reverse(tokenize_success_form.action_url),
context = {
"forms": [tokenize_retry_form, tokenize_success_form],
"cta_button": "tokenize_card",
"card_tokenize_env": agency.payment_processor.card_tokenize_env,
"card_tokenize_func": agency.payment_processor.card_tokenize_func,
"card_tokenize_url": agency.payment_processor.card_tokenize_url,
"token_field": "card_token",
"form_retry": tokenize_retry_form.id,
"form_success": tokenize_success_form.id,
}

logger.debug(f'card_tokenize_url: {context["card_tokenize_url"]}')

return TemplateResponse(request, TEMPLATE_INDEX, context)


Expand Down
Loading