~sircmpwn/names.sr.ht

6ccb9e81ee1dc604bf3523869a49891fcd0d19b5 — Drew DeVault 5 months ago d778737 master
Build out final page of purchase flow
M names/blueprints/contacts.py => names/blueprints/contacts.py +6 -5
@@ 10,17 10,18 @@ from uuid import uuid4

contacts = Blueprint("names.contacts", __name__)

def format_number(n):
    country, number = n.split(".")
    n = phonenumbers.parse(f"+{country} {number}")
    return phonenumbers.format_number(n,
            phonenumbers.PhoneNumberFormat.INTERNATIONAL)

@contacts.route("/contacts")
@loginrequired
def contacts_GET():
    contacts = (DomainContact.query
            .filter(DomainContact.user_id == current_user.id))
    contacts, pagination = paginate_query(contacts)
    def format_number(n):
        country, number = n.split(".")
        n = phonenumbers.parse(f"+{country} {number}")
        return phonenumbers.format_number(n,
                phonenumbers.PhoneNumberFormat.INTERNATIONAL)
    return render_template("contacts.html",
            contacts=contacts, iso3166=iso3166, format_number=format_number,
            **pagination)

M names/blueprints/web.py => names/blueprints/web.py +95 -18
@@ 1,10 1,15 @@
import re
import iso3166
from flask import Blueprint, render_template, request, redirect, url_for, abort
from flask import session
from jinja2 import Markup
from names.blueprints.contacts import format_number
from names.decorators import paidloginrequired
from names.pygments import ZoneLexer
from names.types import User
from pygments import highlight
from pygments.formatters import HtmlFormatter
from srht.config import get_origin
from srht.oauth import current_user, loginrequired, UserType
from srht.validation import Validation



@@ 27,7 32,6 @@ def index():
@web.route("/create")
@loginrequired
def create_GET():
    # TODO: Redirect to user contact info
    return render_template("create.html")

@web.route("/create", methods=["POST"])


@@ 42,19 46,27 @@ def create_POST():
        return render_template("create.html", **valid.kwargs), 400
    return redirect(url_for(".create_domain_GET", domain=domain))

def get_pricing_details(domain, years=1):
    market_price = get_price(domain) * years
    # Factor in our Stripe fees, so we don't lose money
    fees = int((market_price * 0.029) + 30)
    our_price = market_price + fees
    return {
        "market_price": market_price,
        "our_price": our_price,
        "fees": fees,
    }

@web.route("/create/<domain>")
@loginrequired
def create_domain_GET(domain):
    status = lookup_domain(domain)

    # TODO: Check if user has payment info on file
    if status == DomainStatus.available:
        market_price = get_price(domain)
        # Factor in our Stripe fees, so we don't lose money
        our_price = int(market_price + (market_price * 0.029) + 30)
        details = get_pricing_details(domain)
        return render_template("create-domain.html",
                DomainStatus=DomainStatus, status=status, domain=domain,
                our_price=our_price, market_price=market_price)
                **details)
    else:
        transferable, transfer_email = check_transfer(domain)
        # Note: this could be merged into lookup_domain and save us an API call


@@ 69,20 81,9 @@ def create_domain_GET(domain):
@loginrequired
def create_domain_POST(domain):
    valid = Validation(request)
    permitted_users = [
        UserType.active_paying,
        UserType.active_free,
        UserType.admin,
    ]
    if "purchase" in valid:
        if current_user.user_type not in permitted_users:
            return render_template("need-paid-account.html")
        # TODO: Check for contacts
        return render_template("purchase-domain.html")
        return redirect(url_for(".purchase_domain_GET", domain=domain))
    elif "transfer" in valid:
        if current_user.user_type not in permitted_users:
            return render_template("need-paid-account.html")
        # TODO: Check for contacts
        return render_template("transfer-domain.html")
    elif "configure" in valid:
        # TODO: Configure domain


@@ 90,6 91,82 @@ def create_domain_POST(domain):
    else:
        abort(400)

@web.route("/purchase/<domain>")
@paidloginrequired
def purchase_domain_GET(domain):
    # TODO: Pull contacts out of the session if present
    owner_contact = current_user.default_owner
    admin_contact = current_user.default_admin
    billing_contact = current_user.default_billing
    tech_contact = current_user.default_tech
    years = session.get("registration-years", default=1)
    details = get_pricing_details(domain) # cache?
    return render_template("purchase-domain.html",
            owner_contact=owner_contact,
            admin_contact=admin_contact,
            billing_contact=billing_contact,
            tech_contact=tech_contact,
            iso3166=iso3166,
            format_number=format_number,
            years=years, domain=domain, **details)

@web.route("/purchase/<domain>", methods=["POST"])
@paidloginrequired
def purchase_domain_POST(domain):
    # TODO: Pull contacts out of the session if present
    owner_contact = current_user.default_owner
    admin_contact = current_user.default_admin
    billing_contact = current_user.default_billing
    tech_contact = current_user.default_tech

    valid = Validation(request)
    years = valid.require("years")
    try:
        years = int(years)
    except:
        valid.error("Expected registration period to be an integer",
                field="years")
        years = 1
    # TODO: Validate years falls within permissible range for this TLD
    valid.expect(1 <= years <= 10,
            "Invalid registration period", field="years")
    details = get_pricing_details(domain) # cache?

    if not valid.ok:
        return render_template("purchase-domain.html",
                owner_contact=owner_contact,
                admin_contact=admin_contact,
                billing_contact=billing_contact,
                tech_contact=tech_contact,
                iso3166=iso3166,
                format_number=format_number,
                domain=domain, **details, **valid.kwargs)
    session["registration-years"] = years
    return redirect(url_for(".review_purchase_GET", domain=domain))

@web.route("/purchase/review/<domain>")
@paidloginrequired
def review_purchase_GET(domain):
    # TODO: Pull contacts out of the session if present
    owner_contact = current_user.default_owner
    admin_contact = current_user.default_admin
    billing_contact = current_user.default_billing
    tech_contact = current_user.default_tech
    years = session.get("registration-years")
    if not years:
        return redirect(url_for(".purchase_domain_GET", domain=domain))
    if not all([owner_contact, admin_contact, billing_contact, tech_contact]):
        return redirect(url_for(".purchase_domain_GET", domain=domain))
    details = get_pricing_details(domain, years) # cache?
    return render_template("review-purchase.html",
        owner_contact=owner_contact,
        admin_contact=admin_contact,
        billing_contact=billing_contact,
        tech_contact=tech_contact,
        iso3166=iso3166,
        format_number=format_number,
        years=years, domain=domain, **details)

@web.route("/mockup")
def mockup():
    with open("/home/sircmpwn/sr.ht.zone") as f:

A names/decorators.py => names/decorators.py +26 -0
@@ 0,0 1,26 @@
from flask import request, redirect, current_app
from functools import wraps
from srht.config import get_origin
from srht.oauth import current_user, UserType
from urllib.parse import quote_plus

def paidloginrequired(f):
    paid_users = [
        UserType.active_paying,
        UserType.active_free,
        UserType.admin,
    ]

    @wraps(f)
    def wrapper(*args, **kwargs):
        billing_url = (get_origin("meta.sr.ht", external=True)
                + "/billing/initial?return_to="
                + quote_plus(request.url))
        if not current_user:
            return redirect(current_app.oauth_service.oauth_url(request.url))
        elif current_user.user_type not in paid_users:
            return render_template("need-paid-account.html",
                    billing_url=billing_url)
        else:
            return f(*args, **kwargs)
    return wrapper

M names/templates/contacts.html => names/templates/contacts.html +18 -29
@@ 99,42 99,31 @@
  <div class="col-md-4">
    <h3>Default contacts</h3>
    <div class="event-list">
      {% macro display_contact(contact_type, contact) %}
      <div class="event">
        <h4>Owner Contact</h4>
        John Doe &mdash; Acme, Inc<br />
        <a href="#">jdoe@example.org</a><br />
        <h4>{{contact_type}}</h4>
        {% if contact %}
        {{contact.first_name}}
        {{contact.last_name}}
        &mdash;
        {{contact.org_name}}<br />
        <a href="mailto:{{contact.email}}">{{contact.email}}</a><br />
        <span class="text-muted">
          b20968c6-8b99-42a0...
          {{contact.uuid[:18]}}...
        </span>
        {# TODO: Contact review/edit page #}
        <a href="#">{{icon("external-link-alt")}}</a>
      </div>
      <div class="event">
        <h4>Administrative Contact</h4>
        Jane Doe &mdash; Acme, Inc<br />
        <a href="#">jdoe@example.org</a><br />
        <span class="text-muted">
          b20968c6-8b99-42a0...
        </span>
        <a href="#">{{icon("external-link-alt")}}</a>
      </div>
      <div class="event">
        <h4>Billing Contact</h4>
        John Doe &mdash; Acme, Inc<br />
        <a href="#">jdoe@example.org</a><br />
        {% else %}
        <span class="text-muted">
          b20968c6-8b99-42a0...
          No default specified.
        </span>
        <a href="#">{{icon("external-link-alt")}}</a>
      </div>
      <div class="event">
        <h4>Technical Contact</h4>
        Jane Doe &mdash; Acme, Inc<br />
        <a href="#">jdoe@example.org</a><br />
        <span class="text-muted">
          b20968c6-8b99-42a0...
        </span>
        <a href="#">{{icon("external-link-alt")}}</a>
        {% endif %}
      </div>
      {% endmacro %}
      {{display_contact("Owner Contact", current_user.default_owner)}}
      {{display_contact("Administrative Contact", current_user.default_admin)}}
      {{display_contact("Billing Contact", current_user.default_billing)}}
      {{display_contact("Technical Contact", current_user.default_tech)}}
    </div>
    <a
      href="{{url_for("names.contacts.new_GET")}}"

M names/templates/create-domain.html => names/templates/create-domain.html +1 -1
@@ 26,7 26,7 @@
      {{cfg("sr.ht", "site-name")}} account, we don't charge a markup.
    </blockquote>
    <button class="btn btn-primary" name="purchase">
      Purchase this domain {{icon("caret-right")}}
      Configure purchase details {{icon("caret-right")}}
    </button>
    <button class="btn btn-default" name="configure">
      Configure nameservers {{icon("caret-right")}}

M names/templates/need-paid-account.html => names/templates/need-paid-account.html +2 -4
@@ 8,8 8,7 @@
    <p>
      Domain registration is only available to paid accounts. This allows us to
      avoid charging you a markup for our domains - you pay what we pay, plus
      our payment processor's fees. Please fill out your billing information on
      your profile, then refresh this page to continue.
      our payment processor's fees.
    </p>
    {% if current_user.user_type.value == "active_delinquent" %}
    <div class="alert alert-danger">


@@ 19,9 18,8 @@
    </div>
    {% endif %}
    <a
      href="{{get_origin('meta.sr.ht', external=True)}}/billing/initial"
      href="{{billing_url}}"
      class="btn btn-primary"
      target="_blank"
    >Continue to billing {{icon('caret-right')}}</a>
    <a
      href="{{url_for("names.web.index")}}"

A names/templates/purchase-domain.html => names/templates/purchase-domain.html +113 -0
@@ 0,0 1,113 @@
{% extends "layout.html" %}
{% block content %}
<form method="POST">
  <div class="row">
    {{csrf_token()}}
    <div class="col-md-12">
      <h3>
        Configure your purchase
      </h3>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <p>The following domain contacts will be used for {{domain}}:</p>
      <div class="event-list contact-list">
        {% macro display_contact(contact_type, contact, is_default) %}
        <div class="event">
          <h4>
            {{contact_type}}
          </h4>
          {% if contact %}
          {{contact.first_name}}
          {{contact.last_name}}
          &mdash;
          {{contact.org_name}}<br />
          <a href="mailto:{{contact.email}}">{{contact.email}}</a><br />
          {% if is_default %}
          <div class="text-muted">(account default)</div>
          {% else %}
          <div>{{contact.uuid[:18]}}...</div>
          {% endif %}
          <a class="btn btn-default btn-block" href="#">
            Use another contact {{icon('caret-right')}}
          </a>
          {% else %}
          <span class="text-muted">
            No default specified.
          </span>
          <a class="btn btn-primary" href="#">
            Choose or create a contact {{icon('caret-right')}}
          </a>
          {% endif %}
        </div>
        {% endmacro %}
        {{ display_contact("Owner", owner_contact,
          owner_contact == current_user.default_owner) }}
        {{ display_contact("Administrative", admin_contact,
          admin_contact == current_user.default_admin) }}
        {{ display_contact("Billing", billing_contact,
          billing_contact == current_user.default_billing) }}
        {{ display_contact("Technical", tech_contact,
          tech_contact == current_user.default_tech) }}
      </div>
    </div>
  </div>
  <div class="row">
    <div class="col-md-8">
      <div class="form-group">
        <label for="years">Registration period (years)</label>
        <input
          class="form-control {{valid.cls('years')}}"
          type="number"
          value="{{years}}"
          name="years"
          id="years"
          {# TODO: Set min/max based on TLD-specific parameters #}
          min="1"
          max="10"
          required />
        <small class="form-text text-muted">
          <strong>{{domain}}</strong> is available for
          ${{"{:.02f}".format(market_price / 100)}} per year plus
          ${{"{:.02f}".format(fees / 100)}} in payment processing fees.
          As you increase the registration period, you pay a smaller fee per
          dollar spent (2.9% + 30&cent;).
        </small>
        {{valid.summary("years")}}
      </div>
      <div class="form-group">
        <details>
          <summary class="text-muted">
            Additional registration options
          </summary>
          <label class="checkbox" for="privacy">
            <input type="checkbox" name="privacy" id="privacy" checked />
            Enable WHOIS Privacy for this domain
          </label>
          {# TODO: Add nameserver configuration #}
        </details>
      </div>
    </div>
  </div>
  <div class="row" style="margin-top: 1rem">
    <div class="col-md-2">
      <a
        href="{{url_for("names.web.create_domain_GET", domain=domain)}}"
        class="btn btn-default btn-block"
      >{{icon('caret-left')}} Back</a>
    </div>
    <div class="offset-md-3 col-md-3">
      {% if owner_contact and admin_contact and billing_contact and tech_contact %}
      <button class="btn btn-primary btn-block" name="purchase">
        Review purchase {{icon('caret-right')}}
      </button>
      {% else %}
      <button class="btn btn-primary btn-block" name="purchase" disabled>
        Choose contacts to proceed
      </button>
      {% endif %}
    </div>
  </div>
</form>
{% endblock %}

D names/templates/register-domain.html => names/templates/register-domain.html +0 -19
@@ 1,19 0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="row">
  <form class="col-md-12" method="POST">
    <h3>Register {{domain}}</h3>
    <p>
      <strong>{{domain}}</strong> is available for
      ${{"{:.02f}".format(price.cents / 100.0)}} per year!
    </p>
    {{csrf_token()}}
    <button class="btn btn-primary">
      Purchase this domain {{icon("caret-right")}}
    </button>
    <button class="btn btn-default">
      I just want DNS {{icon("caret-right")}}
    </button>
  </form>
</div>
{% endblock %}

A names/templates/review-purchase.html => names/templates/review-purchase.html +89 -0
@@ 0,0 1,89 @@
{% extends "layout.html" %}
{% block content %}
<form method="POST">
  <div class="row">
    {{csrf_token()}}
    <div class="col-md-12">
      <h3>
        Review your purchase
      </h3>
    </div>
  </div>
  <div class="row">
    <div class="col-md-12">
      <div class="event-list contact-list">
        {% macro display_contact(contact_type, contact, is_default) %}
        <div class="event">
          <h4>
            {{contact_type}}
          </h4>
          {{contact.first_name}}
          {{contact.last_name}}
          &mdash;
          {{contact.org_name}}<br />
          <a href="mailto:{{contact.email}}">{{contact.email}}</a><br />
        </div>
        {% endmacro %}
        {{ display_contact("Owner", owner_contact,
          owner_contact == current_user.default_owner) }}
        {{ display_contact("Administrative", admin_contact,
          admin_contact == current_user.default_admin) }}
        {{ display_contact("Billing", billing_contact,
          billing_contact == current_user.default_billing) }}
        {{ display_contact("Technical", tech_contact,
          tech_contact == current_user.default_tech) }}
      </div>
    </div>
  </div>
  <div class="row">
    <div class="offset-md-2 col-md-8">
      <table class="table">
        <thead>
          <tr>
            <th>Item</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            {% if years == 1 %}
            <td>{{domain}} registration ({{years}} year)</td>
            {% else %}
            <td>{{domain}} registration ({{years}} years)</td>
            {% endif %}
            <td
              style="text-align: right"
            >${{"{:.02f}".format(market_price / 100.0)}}</td>
          </tr>
          <tr>
            <td>Processing fees</td>
            <td
              style="text-align: right"
            >${{"{:.02f}".format(fees / 100.0)}}</td>
          </tr>
          <tr>
            <td style="border: none; text-align: right">Total</td>
            <td
              style="text-align: right; font-weight: bold"
            >${{"{:.02f}".format(our_price / 100.0)}}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
  <div class="row">
    <div class="offset-md-2 col-md-2">
      <a
        href="{{url_for("names.web.purchase_domain_GET", domain=domain)}}"
        class="btn btn-default btn-block"
      >{{icon('caret-left')}} Make changes</a>
    </div>
    <div class="offset-md-3 col-md-3">
      <button class="btn btn-primary btn-block">
        Complete purchase {{icon('caret-right')}}
      </button>
    </div>
  </div>
</form>
{% endblock %}


M scss/main.scss => scss/main.scss +22 -0
@@ 31,3 31,25 @@ h4 small {
    }
  }
}

.contact-list {
  display: flex;
  margin: 0 -15px 1rem;

  .event {
    width: calc(25% - 30px);
    margin: 0 15px;
  }

  .btn-block {
    margin-top: 0.5rem;
  }
}

.registration-term {
  display: none;
}

#years[value="1"] ~ .registration-term.term-1-years {
  display: table;
}