diff --git a/templates/zilencer/payment.html b/templates/zilencer/payment.html new file mode 100644 index 0000000000..9fc57f1036 --- /dev/null +++ b/templates/zilencer/payment.html @@ -0,0 +1,96 @@ +{% extends "zerver/portico.html" %} + +{% block customhead %} + +{% stylesheet 'portico' %} +{% stylesheet 'landing-page' %} +{{ render_bundle('landing-page') }} + +{% endblock %} + +{% block portico_content %} + +{% include 'zerver/gradients.html' %} +{% include 'zerver/landing_nav.html' %} + +
+
+ {% if error_message %} +
+ {{ error_message }} +
+ {% else %} +
+
+
+
+
+ Zulip Cloud subscription for {{ realm_name }} +
+ {% if payment_method_added %} +
+ The card has been saved successfully. +
+ {% endif %} + {% if num_cards %} +
+ {% if num_cards > 1 %} + You have {{ num_cards }} saved cards. + {% else %} + You have one saved card. + {% endif %} +
+ {% endif %} +
+
+

Premium

+
+ Make Zulip your home +
+
+
    +
  • Full search history
  • +
  • File storage up to 10 GB per user
  • +
  • Full access to enterprise features like Google and GitHub OAuth
  • +
  • Priority commercial support
  • +
  • Funds the Zulip open source project
  • +
+
+
+
+
+
8
+
+ per active user, per month +
+ "$80/year billed annually" +
+
+
+ {{ csrf_input }} + +
+
+
+
+
+
+
+
+ {% endif %} +
+
+{% endblock %} diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index 88212db794..28fa86afab 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -527,6 +527,7 @@ def build_custom_checkers(by_lang): 'bad_lines': ['']}, {'pattern': 'script src="http', 'description': "Don't directly load dependencies from CDNs. See docs/subsystems/front-end-build-process.md", + 'exclude': set(["templates/zilencer/payment.html"]), 'good_lines': ["{{ render_bundle('landing-page') }}"], 'bad_lines': ['']}, {'pattern': "title='[^{]", diff --git a/zilencer/urls.py b/zilencer/urls.py index b69b1af6c7..8f3d8fb089 100644 --- a/zilencer/urls.py +++ b/zilencer/urls.py @@ -5,7 +5,9 @@ from django.conf.urls import include, url import zilencer.views from zerver.lib.rest import rest_dispatch -i18n_urlpatterns = [] # type: Any +i18n_urlpatterns = [ + url(r'^billing/$', zilencer.views.add_payment_method), +] # type: Any # Zilencer views following the REST API style v1_api_and_json_patterns = [ diff --git a/zilencer/views.py b/zilencer/views.py index 2a01423515..be8a0b95e4 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -1,10 +1,19 @@ +import logging from typing import Any, Dict, Optional, Text, Union, cast from django.http import HttpRequest, HttpResponse from django.utils import timezone from django.utils.translation import ugettext as _ +from django.shortcuts import render +from django.conf import settings +from django.views.decorators.http import require_GET +from django.views.decorators.csrf import csrf_exempt +import stripe +from stripe.error import CardError, RateLimitError, InvalidRequestError, \ + AuthenticationError, APIConnectionError, StripeError +from zerver.decorator import require_post, zulip_login_required from zerver.lib.exceptions import JsonableError from zerver.lib.push_notifications import send_android_push_notification, \ send_apple_push_notification @@ -13,7 +22,12 @@ from zerver.lib.response import json_error, json_success from zerver.lib.validator import check_int from zerver.models import UserProfile from zerver.views.push_notifications import validate_token -from zilencer.models import RemotePushDeviceToken, RemoteZulipServer +from zilencer.models import RemotePushDeviceToken, RemoteZulipServer, Customer +from zproject.settings import get_secret + +STRIPE_SECRET_KEY = get_secret('stripe_secret_key') +STRIPE_PUBLISHABLE_KEY = get_secret('stripe_publishable_key') +stripe.api_key = STRIPE_SECRET_KEY def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> None: if not isinstance(entity, RemoteZulipServer): @@ -96,3 +110,69 @@ def remote_server_notify_push(request: HttpRequest, entity: Union[UserProfile, R send_apple_push_notification(user_id, apple_devices, apns_payload) return json_success() + + +@zulip_login_required +def add_payment_method(request: HttpRequest) -> HttpResponse: + user = request.user + ctx = { + "publishable_key": STRIPE_PUBLISHABLE_KEY, + "email": user.email, + } # type: Dict[str, Any] + if not user.is_realm_admin: + ctx["error_message"] = _("You should be an administrator of the organization %s to view this page." + % (user.realm.name,)) + return render(request, 'zilencer/payment.html', context=ctx) + + try: + if request.method == "GET": + try: + customer_obj = Customer.objects.get(realm=user.realm) + cards = stripe.Customer.retrieve(customer_obj.stripe_customer_id).sources.all(object="card") + ctx["num_cards"] = len(cards["data"]) + except Customer.DoesNotExist: + ctx["num_cards"] = 0 + return render(request, 'zilencer/payment.html', context=ctx) + + if request.method == "POST": + token = request.POST.get("stripeToken", "") + # The card metadata doesn't show up in Dashboard but can be accessed + # using the API. + card_metadata = {"added_user_id": user.id, "added_user_email": user.email} + try: + customer_obj = Customer.objects.get(realm=user.realm) + customer = stripe.Customer.retrieve(customer_obj.stripe_customer_id) + customer.sources.create(source=token, metadata=card_metadata) + ctx["num_cards"] = len(customer.sources.all(object="card")["data"]) + except Customer.DoesNotExist: + customer_metadata = {"string_id": user.realm.string_id} + # Description makes it easier to identify customers in Stripe dashboard + description = "{} ({})".format(user.realm.name, user.realm.string_id) + customer = stripe.Customer.create(source=token, + description=description, + metadata=customer_metadata) + + card = customer.sources.all(object="card")["data"][0] + card.metadata = card_metadata + card.save() + Customer.objects.create(realm=user.realm, stripe_customer_id=customer.id) + ctx["num_cards"] = 1 + ctx["payment_method_added"] = True + return render(request, 'zilencer/payment.html', context=ctx) + except (CardError, RateLimitError, APIConnectionError) as e: + err = e.json_body.get('error', {}) + logging.error("Stripe error - Status: {}, Type: {}, Code: {}, Param: {}, Message: {}".format( + e.http_status, err.get('type'), err.get('code'), err.get('param'), err.get('message') + )) + ctx["error_message"] = err.get('message') + return render(request, 'zilencer/payment.html', context=ctx) + except (InvalidRequestError, AuthenticationError, StripeError) as e: + err = e.json_body.get('error', {}) + logging.error("Stripe error - Status: {}, Type: {}, Code: {}, Param: {}, Message: {}".format( + e.http_status, err.get('type'), err.get('code'), err.get('param'), err.get('message') + )) + except Exception as e: + logging.error('Stripe error: %s' % (str(e),)) + ctx["error_message"] = _("Something went wrong. Please try again or email us at %s." + % (settings.ZULIP_ADMINISTRATOR,)) + return render(request, 'zilencer/payment.html', context=ctx) diff --git a/zproject/settings.py b/zproject/settings.py index f29b08c568..a4bed7dc04 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -15,6 +15,7 @@ import os import platform import time import sys +from typing import Optional import configparser from zerver.lib.db import TimeTrackingConnection @@ -39,7 +40,7 @@ if PRODUCTION: else: secrets_file.read(os.path.join(DEPLOY_ROOT, "zproject/dev-secrets.conf")) -def get_secret(key: str) -> None: +def get_secret(key: str) -> Optional[str]: if secrets_file.has_option('secrets', key): return secrets_file.get('secrets', key) return None