diff --git a/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.3.json new file mode 100644 index 0000000000..2aee093b59 --- /dev/null +++ b/corporate/tests/stripe_fixtures/free_trial_upgrade_by_card--Customer.retrieve.3.json @@ -0,0 +1,104 @@ +{ + "account_balance": 0, + "address": null, + "balance": 0, + "created": 1010000001, + "currency": null, + "default_source": { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "pass", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "pass", + "brand": "Visa", + "country": "US", + "customer": "cus_NORMALIZED0001", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "NORMALIZED000001", + "funding": "credit", + "id": "card_NORMALIZED00000000000001", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + }, + "delinquent": false, + "description": "zulip (Zulip Dev)", + "discount": null, + "email": "hamlet@zulip.com", + "id": "cus_NORMALIZED0001", + "invoice_prefix": "NORMA01", + "invoice_settings": { + "custom_fields": null, + "default_payment_method": null, + "footer": null + }, + "livemode": false, + "metadata": { + "realm_id": "1", + "realm_str": "zulip" + }, + "name": null, + "next_invoice_sequence": 1, + "object": "customer", + "phone": null, + "preferred_locales": [], + "shipping": null, + "sources": { + "data": [ + { + "address_city": "Pacific", + "address_country": "United States", + "address_line1": "Under the sea,", + "address_line1_check": "pass", + "address_line2": null, + "address_state": null, + "address_zip": "33333", + "address_zip_check": "pass", + "brand": "Visa", + "country": "US", + "customer": "cus_NORMALIZED0001", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 3, + "exp_year": 2033, + "fingerprint": "NORMALIZED000001", + "funding": "credit", + "id": "card_NORMALIZED00000000000001", + "last4": "4242", + "metadata": {}, + "name": "Ada Starr", + "object": "card", + "tokenization_method": null + } + ], + "has_more": false, + "object": "list", + "total_count": 1, + "url": "/v1/customers/cus_NORMALIZED0001/sources" + }, + "subscriptions": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_NORMALIZED0001/subscriptions" + }, + "tax_exempt": "none", + "tax_ids": { + "data": [], + "has_more": false, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_NORMALIZED0001/tax_ids" + }, + "tax_info": null, + "tax_info_verification": null + } diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index ead18f3d39..5e2517afaa 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -625,6 +625,11 @@ class StripeTest(StripeTestCase): 'Visa ending in 4242', 'Update card']: self.assert_in_response(substring, response) + self.assert_not_in_success_response(["Go to your Zulip organization"], response) + + with patch('corporate.views.timezone_now', return_value=self.now): + response = self.client_get("/billing/?onboarding=true") + self.assert_in_success_response(["Go to your Zulip organization"], response) with patch('corporate.lib.stripe.get_latest_seat_count', return_value=12): update_license_ledger_if_needed(realm, self.now) @@ -1053,6 +1058,33 @@ class StripeTest(StripeTestCase): self.assertEqual(response.status_code, 302) self.assertEqual('/upgrade/', response.url) + def test_redirect_for_upgrade_page(self) -> None: + user = self.example_user("iago") + self.login_user(user) + # No Customer yet; + response = self.client_get("/upgrade/") + self.assertEqual(response.status_code, 200) + + # Customer, but no CustomerPlan; + customer = Customer.objects.create(realm=user.realm, stripe_customer_id='cus_123') + response = self.client_get("/upgrade/") + self.assertEqual(response.status_code, 200) + + CustomerPlan.objects.create(customer=customer, billing_cycle_anchor=timezone_now(), + billing_schedule=CustomerPlan.ANNUAL, tier=CustomerPlan.STANDARD) + response = self.client_get("/upgrade/") + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/billing/") + + with self.settings(FREE_TRIAL_DAYS=30): + response = self.client_get("/upgrade/") + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/billing/") + + response = self.client_get("/upgrade/?onboarding=true") + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/billing/?onboarding=true") + def test_get_latest_seat_count(self) -> None: realm = get_realm("zulip") initial_count = get_latest_seat_count(realm) diff --git a/corporate/views.py b/corporate/views.py index 6d186ab53c..1ee03762d7 100644 --- a/corporate/views.py +++ b/corporate/views.py @@ -129,7 +129,10 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse: user = request.user customer = get_customer_by_realm(user.realm) if customer is not None and get_current_plan_by_customer(customer) is not None: - return HttpResponseRedirect(reverse('corporate.views.billing_home')) + billing_page_url = reverse('corporate.views.billing_home') + if request.GET.get("onboarding") is not None: + billing_page_url = "{}?onboarding=true".format(billing_page_url) + return HttpResponseRedirect(billing_page_url) percent_off = Decimal(0) if customer is not None and customer.default_discount is not None: @@ -147,6 +150,7 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse: 'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE, 'plan': "Zulip Standard", "free_trial_days": settings.FREE_TRIAL_DAYS, + "onboarding": request.GET.get("onboarding") is not None, 'page_params': { 'seat_count': seat_count, 'annual_price': 8000, @@ -212,6 +216,7 @@ def billing_home(request: HttpRequest) -> HttpResponse: 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, 'CustomerPlan': CustomerPlan, + 'onboarding': request.GET.get("onboarding") is not None, }) return render(request, 'corporate/billing.html', context=context) diff --git a/frontend_tests/node_tests/billing_helpers.js b/frontend_tests/node_tests/billing_helpers.js index d7eaaf1dbe..feda55446e 100644 --- a/frontend_tests/node_tests/billing_helpers.js +++ b/frontend_tests/node_tests/billing_helpers.js @@ -25,6 +25,7 @@ run_test('create_ajax_request', () => { const form_success = "#autopay-success"; const form_error = "#autopay-error"; const form_loading = "#autopay-loading"; + const zulip_limited_section = "#zulip-limited-section"; const state = { form_input_section_show: 0, @@ -34,6 +35,8 @@ run_test('create_ajax_request', () => { form_loading_show: 0, form_loading_hide: 0, form_success_show: 0, + zulip_limited_section_show: 0, + zulip_limited_section_hide: 0, location_reload: 0, pushState: 0, make_indicator: 0, @@ -79,6 +82,14 @@ run_test('create_ajax_request', () => { state.form_loading_hide += 1; }; + $(zulip_limited_section).show = () => { + state.zulip_limited_section_show += 1; + }; + + $(zulip_limited_section).hide = () => { + state.zulip_limited_section_hide += 1; + }; + $("#autopay-form").serializeArray = () => { return jquery("#autopay-form").serializeArray(); }; @@ -87,6 +98,8 @@ run_test('create_ajax_request', () => { assert.equal(state.form_input_section_hide, 1); assert.equal(state.form_error_hide, 1); assert.equal(state.form_loading_show, 1); + assert.equal(state.zulip_limited_section_hide, 1); + assert.equal(state.zulip_limited_section_show, 0); assert.equal(state.make_indicator, 1); assert.equal(url, "/json/billing/upgrade"); @@ -119,12 +132,15 @@ run_test('create_ajax_request', () => { assert.equal(state.form_success_show, 1); assert.equal(state.form_error_hide, 2); assert.equal(state.form_loading_hide, 1); + assert.equal(state.zulip_limited_section_hide, 1); + assert.equal(state.zulip_limited_section_show, 0); error({responseText: '{"msg": "response_message"}'}); assert.equal(state.form_loading_hide, 2); assert.equal(state.form_error_show, 1); assert.equal(state.form_input_section_show, 1); + assert.equal(state.zulip_limited_section_hide, 1); }; helpers.create_ajax_request("/json/billing/upgrade", "autopay", {id: "stripe_token_id"}, ["licenses"]); diff --git a/frontend_tests/node_tests/upgrade.js b/frontend_tests/node_tests/upgrade.js index 510afd3e9a..2d327aa0ff 100644 --- a/frontend_tests/node_tests/upgrade.js +++ b/frontend_tests/node_tests/upgrade.js @@ -102,6 +102,7 @@ run_test("initialize", () => { helpers.is_valid_input = () => { return true; }; + add_card_click_handler(e); invoice_click_handler(e); @@ -159,6 +160,8 @@ run_test("autopay_form_fields", () => { assert(document.querySelector("#autopay_loading_indicator")); assert(document.querySelector("input[name=csrfmiddlewaretoken]")); + + assert(document.querySelector("#zulip-limited-section")); }); run_test("invoice_form_fields", () => { @@ -178,4 +181,6 @@ run_test("invoice_form_fields", () => { assert(document.querySelector("#invoice_loading_indicator")); assert(document.querySelector("input[name=csrfmiddlewaretoken]")); + + assert(document.querySelector("#zulip-limited-section")); }); diff --git a/static/js/billing/helpers.js b/static/js/billing/helpers.js index ff874273dc..3aaddffb1c 100644 --- a/static/js/billing/helpers.js +++ b/static/js/billing/helpers.js @@ -6,11 +6,16 @@ exports.create_ajax_request = function (url, form_name, stripe_token = null, num const form_error = "#" + form_name + "-error"; const form_loading = "#" + form_name + "-loading"; + const zulip_limited_section = "#zulip-limited-section"; + const free_trial_alert_message = "#free-trial-alert-message"; + loading.make_indicator($(form_loading_indicator), {text: 'Processing ...', abs_positioned: true}); $(form_input_section).hide(); $(form_error).hide(); $(form_loading).show(); + $(zulip_limited_section).hide(); + $(free_trial_alert_message).hide(); const data = {}; if (stripe_token) { @@ -45,6 +50,8 @@ exports.create_ajax_request = function (url, form_name, stripe_token = null, num $(form_loading).hide(); $(form_error).show().text(JSON.parse(xhr.responseText).msg); $(form_input_section).show(); + $(zulip_limited_section).show(); + $(free_trial_alert_message).show(); }, }); }; diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index 485fb14068..e5c8bb6842 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -124,6 +124,15 @@
Contact support@zulipchat.com diff --git a/templates/corporate/upgrade.html b/templates/corporate/upgrade.html index d534fe532e..82230614cf 100644 --- a/templates/corporate/upgrade.html +++ b/templates/corporate/upgrade.html @@ -207,6 +207,15 @@
We're happy to help! diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 770d94063f..39f415c112 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -2150,6 +2150,10 @@ class RealmCreationTest(ZulipTestCase): HTTP_HOST=string_id + ".testserver") self.assertEqual(result.status_code, 302) + result = self.client_get(result.url, subdomain=string_id) + self.assertEqual(result.status_code, 302) + self.assertEqual(result.url, 'http://zuliptest.testserver') + # Make sure the realm is created realm = get_realm(string_id) self.assertEqual(realm.string_id, string_id) @@ -2158,6 +2162,46 @@ class RealmCreationTest(ZulipTestCase): self.assertEqual(realm.name, realm_name) self.assertEqual(realm.subdomain, string_id) + @override_settings(OPEN_REALM_CREATION=True, FREE_TRIAL_DAYS=30) + def test_create_realm_during_free_trial(self) -> None: + password = "test" + string_id = "zuliptest" + email = "user1@test.com" + realm_name = "Test" + + with self.assertRaises(Realm.DoesNotExist): + get_realm(string_id) + + result = self.client_post('/new/', {'email': email}) + self.assertEqual(result.status_code, 302) + self.assertTrue(result["Location"].endswith( + "/accounts/new/send_confirm/%s" % (email,))) + result = self.client_get(result["Location"]) + self.assert_in_response("Check your email so we can get started.", result) + + confirmation_url = self.get_confirmation_url_from_outbox(email) + result = self.client_get(confirmation_url) + self.assertEqual(result.status_code, 200) + + result = self.submit_reg_form_for_user(email, password, + realm_subdomain = string_id, + realm_name=realm_name, + HTTP_HOST=string_id + ".testserver") + self.assertEqual(result.status_code, 302) + + result = self.client_get(result.url, subdomain=string_id) + self.assertEqual(result.url, 'http://zuliptest.testserver/upgrade/?onboarding=true') + + result = self.client_get(result.url, subdomain=string_id) + self.assert_in_success_response(["Or start with Zulip Limited (Free) plan"], result) + + realm = get_realm(string_id) + self.assertEqual(realm.string_id, string_id) + self.assertEqual(get_user(email, realm).realm, realm) + + self.assertEqual(realm.name, realm_name) + self.assertEqual(realm.subdomain, string_id) + @override_settings(OPEN_REALM_CREATION=True) def test_mailinator_signup(self) -> None: result = self.client_post('/new/', {'email': "hi@mailinator.com"}) diff --git a/zerver/views/auth.py b/zerver/views/auth.py index 377b19ff21..708d6ee04b 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -218,6 +218,7 @@ def register_remote_user(request: HttpRequest, result: ExternalAuthResult) -> Ht # maybe_send_to_registration doesn't take these arguments, so delete them. kwargs.pop('subdomain', None) kwargs.pop('redirect_to', None) + kwargs.pop('is_realm_creation', None) kwargs["password_required"] = False return maybe_send_to_registration(request, **kwargs) @@ -247,6 +248,7 @@ def login_or_register_remote_user(request: HttpRequest, result: ExternalAuthResu # Otherwise, the user has successfully authenticated to an # account, and we need to do the right thing depending whether # or not they're using the mobile OTP flow or want a browser session. + is_realm_creation = result.data_dict.get('is_realm_creation') mobile_flow_otp = result.data_dict.get('mobile_flow_otp') desktop_flow_otp = result.data_dict.get('desktop_flow_otp') if mobile_flow_otp is not None: @@ -256,7 +258,11 @@ def login_or_register_remote_user(request: HttpRequest, result: ExternalAuthResu do_login(request, user_profile) - redirect_to = get_safe_redirect_to(result.data_dict.get('redirect_to', ''), user_profile.realm.uri) + redirect_to = result.data_dict.get('redirect_to', '') + if is_realm_creation is not None and settings.FREE_TRIAL_DAYS not in [None, 0]: + redirect_to = "{}?onboarding=true".format(reverse('corporate.views.initial_upgrade')) + + redirect_to = get_safe_redirect_to(redirect_to, user_profile.realm.uri) return HttpResponseRedirect(redirect_to) def finish_desktop_flow(request: HttpRequest, user_profile: UserProfile, diff --git a/zerver/views/registration.py b/zerver/views/registration.py index f785cc68b8..badf7c6aef 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -343,7 +343,8 @@ def accounts_register(request: HttpRequest) -> HttpResponse: # Because for realm creation, registration happens on the # root domain, we need to log them into the subdomain for # their new realm. - return redirect_and_log_into_subdomain(ExternalAuthResult(user_profile=user_profile)) + return redirect_and_log_into_subdomain(ExternalAuthResult(user_profile=user_profile, + data_dict={'is_realm_creation': True})) # This dummy_backend check below confirms the user is # authenticating to the correct subdomain. diff --git a/zproject/backends.py b/zproject/backends.py index bbfd97c4a5..044ad871ba 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -920,6 +920,7 @@ ExternalAuthDataDict = TypedDict('ExternalAuthDataDict', { 'full_name': str, 'email': str, 'is_signup': bool, + 'is_realm_creation': bool, 'redirect_to': str, 'mobile_flow_otp': Optional[str], 'desktop_flow_otp': Optional[str],