Merge pull request #6599 from overleaf/ta-new-subscription-split-test

Payment Page Split Test

GitOrigin-RevId: bb43cbf4e5722bd18076f2f8bf1014816bce1df0
This commit is contained in:
Timothée Alby 2022-02-10 10:52:03 +01:00 committed by Copybot
parent 21c92a045d
commit 0a27b3711f
5 changed files with 419 additions and 1 deletions

View file

@ -114,7 +114,15 @@ async function paymentPage(req, res) {
if (recommendedCurrency && currency == null) {
currency = recommendedCurrency
}
res.render('subscriptions/new', {
const assignment = await SplitTestHandler.promises.getAssignment(
req,
'payment-page'
)
const template =
assignment && assignment.variant === 'updated-payment-page'
? 'subscriptions/new-updated'
: 'subscriptions/new'
res.render(template, {
title: 'subscribe',
currency,
countryCode,

View file

@ -0,0 +1,363 @@
extends ../layout
block append meta
meta(name="ol-countryCode" content=countryCode)
meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
meta(name="ol-recomendedCurrency" content=String(currency).slice(0,3))
block head-scripts
script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
block content
main.content.content-alt#main-content
.container(ng-controller="NewSubscriptionController" ng-cloak)
.row.card-group
.col-md-3.col-md-push-1
if showStudentPlan
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("special_price_student")}
.card.card-first
.price-feature-description
h4(ng-if="planName") {{planName}}
h4(ng-if="!planName") #{plan.name}
if plan.features
ul.small
if plan.features.collaborators === 1
li #{translate("collabs_per_proj_single", {collabcount: 1})}
if plan.features.collaborators === -1
li #{translate("unlimited_collabs")}
if plan.features.collaborators > 1
li #{translate("collabs_per_proj", {collabcount: plan.features.collaborators})}
if plan.features.compileTimeout > 1
li #{translate("increased_compile_timeout")}
if plan.features.dropbox && plan.features.github
li #{translate("sync_dropbox_github")}
if plan.features.versioning
li #{translate("full_doc_history")}
if plan.features.trackChanges
li #{translate("track_changes")}
if plan.features.references
li #{translate("reference_search")}
if plan.features.mendeley || plan.features.zotero
li #{translate("reference_sync")}
if plan.features.symbolyPalette
li #{translate("symboly_palette")}
div.price-summary(ng-if="recurlyPrice")
- var priceVars = { price: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}"};
hr
h4 #{translate("payment_summary")}
div.small
.price-summary-line
span
| {{planName}}
span(ng-if="coupon")
| {{ availableCurrencies[currencyCode]['symbol'] }}{{ coupon.normalPriceWithoutTax | number:2 }}
span(ng-if="!coupon")
| {{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.subtotal }}
.price-summary-line(ng-if="coupon")
span
| {{ coupon.name }}
span
| –{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.discount}}
.price-summary-line(ng-if="taxes && taxes[0] && taxes[0].rate > 0")
span
| #{translate("vat")} {{taxes[0].rate * 100}}%
span
| {{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.tax }}
.price-summary-line.price-summary-total-line
span
b {{ monthlyBilling ? '#{translate("total_per_month")}' : '#{translate("total_per_year")}'}}
span
b {{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}
div.small.price-details-spacing(ng-if="trialLength || coupon")
div.small(ng-if="trialLength") !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})}
div.small(ng-if="recurlyPrice")
- var priceVars = { price: "{{ availableCurrencies[currencyCode]['symbol'] }}{{ recurlyPrice.total }}", discountMonths: "{{ coupon.discountMonths }}" };
span(ng-if="!coupon.singleUse && coupon.discountMonths > 0 && monthlyBilling")
| !{translate("x_price_for_y_months", priceVars, ['strong'] )}
span(ng-if="coupon.singleUse && monthlyBilling")
| !{translate("x_price_for_first_month", priceVars, ['strong'] )}
span(ng-if="coupon.singleUse && !monthlyBilling")
| !{translate("x_price_for_first_year", priceVars, ['strong'] )}
div.small(ng-if="coupon && coupon.normalPrice")
- var noDiscountPriceAngularExp = "{{ availableCurrencies[currencyCode]['symbol']}}{{coupon.normalPrice | number:2 }}";
span(ng-if="!coupon.singleUse && coupon.discountMonths > 0 && monthlyBilling")
| !{translate("then_x_price_per_month", { price: noDiscountPriceAngularExp } )}
span(ng-if="!coupon.singleUse && !coupon.discountMonths && monthlyBilling")
| !{translate("normally_x_price_per_month", { price: noDiscountPriceAngularExp } )}
span(ng-if="!coupon.singleUse && !monthlyBilling")
| !{translate("normally_x_price_per_year", { price: noDiscountPriceAngularExp } )}
span(ng-if="coupon.singleUse && monthlyBilling")
| !{translate("then_x_price_per_month", { price: noDiscountPriceAngularExp } )}
span(ng-if="coupon.singleUse && !monthlyBilling")
| !{translate("then_x_price_per_year", { price: noDiscountPriceAngularExp } )}
hr
p.price-cancel-anytime.text-center(ng-non-bindable) !{translate("cancel_anytime", { appName:'{{settings.appName}}' })}
.col-md-5.col-md-push-1
.card.card-highlighted.card-border(ng-hide="threeDSecureFlow")
.alert.alert-danger(ng-show="recurlyLoadError")
strong #{translate('payment_provider_unreachable_error')}
.price-switch-header(ng-hide="recurlyLoadError")
.row
.col-xs-9
h2 {{planName}}
.col-xs-3
div.dropdown.changePlanButton.pull-right(ng-cloak, dropdown)
a.btn.btn-default.dropdown-toggle(
href="#",
data-toggle="dropdown",
dropdown-toggle
)
| {{currencyCode}} ({{allCurrencies[currencyCode]['symbol']}})
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat="(currency, value) in availableCurrencies")
a(
ng-click="changeCurrency(currency)",
) {{currency}} ({{value['symbol']}})
.row(ng-if="planCode == 'student-annual' || planCode == 'student-monthly' || planCode == 'student_free_trial_7_days'")
.col-xs-12
p.student-disclaimer #{translate('student_disclaimer')}
.row(ng-hide="recurlyLoadError")
div()
.col-md-12()
form(
name="simpleCCForm"
novalidate
)
div.payment-method-toggle
a.payment-method-toggle-switch(
href
ng-click="setPaymentMethod('credit_card');"
ng-class="paymentMethod.value === 'credit_card' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-mastercard.fa-2x(aria-hidden="true")
span  
i.fa.fa-cc-visa.fa-2x(aria-hidden="true")
span  
i.fa.fa-cc-amex.fa-2x(aria-hidden="true")
span.sr-only Pay with Mastercard, Visa, or Amex
a.payment-method-toggle-switch(
href
ng-click="setPaymentMethod('paypal');"
ng-class="paymentMethod.value === 'paypal' ? 'payment-method-toggle-switch-selected' : ''"
)
i.fa.fa-cc-paypal.fa-2x(aria-hidden="true")
span.sr-only Pay with PayPal
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
.alert.alert-warning.small(ng-show="couponError")
strong {{couponError}}
div(ng-show="paymentMethod.value === 'credit_card'")
.row
.col-xs-6
.form-group(ng-class="validation.errorFields.first_name || inputHasError(simpleCCForm.firstName) ? 'has-error' : ''")
label(for="first-name") #{translate('first_name')}
input#first-name.form-control(
type="text"
maxlength='255'
data-recurly="first_name"
name="firstName"
ng-model="data.first_name"
required
)
span.input-feedback-message(ng-if="simpleCCForm.firstName.$error.required") #{translate('this_field_is_required')}
.col-xs-6
.form-group(ng-class="validation.errorFields.last_name || inputHasError(simpleCCForm.lastName)? 'has-error' : ''")
label(for="last-name") #{translate('last_name')}
input#last-name.form-control(
type="text"
maxlength='255'
data-recurly="last_name"
name="lastName"
ng-model="data.last_name"
required
)
span.input-feedback-message(ng-if="simpleCCForm.lastName.$error.required") #{translate('this_field_is_required')}
.form-group(ng-class="validation.errorFields.number ? 'has-error' : ''")
label(for="card-no") #{translate("credit_card_number")}
div#card-no(
type="text"
name="ccNumber"
data-recurly='number'
)
.row
.col-xs-3
.form-group.has-feedback(ng-class="validation.errorFields.month ? 'has-error' : ''")
label(for="month").capitalised #{translate("month")}
div(
type="number"
name="month"
data-recurly="month"
)
.col-xs-3
.form-group.has-feedback(ng-class="validation.errorFields.year ? 'has-error' : ''")
label(for="year").capitalised #{translate("year")}
div(
type="number"
name="year"
data-recurly="year"
)
.col-xs-6
.form-group.has-feedback(ng-class="validation.errorFields.cvv ? 'has-error' : ''")
label #{translate("security_code")}
div(
type="number"
ng-model="data.cvv"
data-recurly="cvv"
name="cvv"
cc-format-sec-code
)
.form-control-feedback
a.form-helper(
href
tabindex="-1"
tooltip-template="'cvv-tooltip-tpl.html'"
tooltip-trigger="mouseenter"
tooltip-append-to-body="true"
) ?
div
.row
.col-xs-12
.form-group(ng-class="validation.errorFields.address1 || inputHasError(simpleCCForm.address1) ? 'has-error' : ''")
label(for="address-line-1") #{translate('address_line_1')}
input#address-line-1.form-control(
type="text"
maxlength="255"
data-recurly="address1"
name="address1"
ng-model="data.address1"
required
)
span.input-feedback-message(ng-if="simpleCCForm.address1.$error.required") #{translate('this_field_is_required')}
.row
.col-xs-12
.form-group.has-feedback(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
label(for="address-line-2") #{translate('address_line_2')}
input#address-line-2.form-control(
type="text"
maxlength="255"
data-recurly="address2"
name="address2"
ng-model="data.address2"
)
.row
.col-xs-4
.form-group(ng-class="validation.errorFields.postal_code || inputHasError(simpleCCForm.postalCode) ? 'has-error' : ''")
label(for="postal-code") #{translate('postal_code')}
input#postal-code.form-control(
type="text"
maxlength="255"
data-recurly="postal_code"
name="postalCode"
ng-model="data.postal_code"
required
)
span.input-feedback-message(ng-if="simpleCCForm.postalCode.$error.required") #{translate('this_field_is_required')}
.col-xs-8
.form-group(ng-class="validation.errorFields.country || inputHasError(simpleCCForm.country) ? 'has-error' : ''")
label(for="country") #{translate('country')}
select#country.form-control(
data-recurly="country"
ng-model="data.country"
name="country"
ng-change="updateCountry()"
ng-selected="{{country.code == data.country}}"
ng-model-options="{ debounce: 200 }"
required
)
option(value='', disabled) #{translate("country")}
option(value='-', disabled) --------------
option(ng-repeat="country in countries" ng-bind-html="country.name" value="{{country.code}}")
span.input-feedback-message(ng-if="simpleCCForm.country.$error.required") #{translate('this_field_is_required')}
.form-group
.checkbox
label
input(
type="checkbox"
ng-model="ui.addCompanyDetails"
)
|
| #{translate("add_company_details")}
.form-group(ng-show="ui.addCompanyDetails")
label(for="company-name") #{translate("company_name")}
input#company-name.form-control(
type="text"
name="companyName"
ng-model="data.company"
)
.form-group(ng-show="ui.addCompanyDetails && taxes.length")
label(for="vat-number") #{translate("vat_number")}
input#vat-number.form-control(
type="text"
name="vatNumber"
ng-model="data.vat_number"
ng-blur="applyVatNumber()"
)
if (showCouponField)
.form-group
label(for="coupon-code") #{translate('coupon_code')}
input#coupon-code.form-control(
type="text"
ng-blur="applyCoupon()"
ng-model="data.coupon"
)
p(ng-if="paymentMethod.value === 'paypal'") #{translate("paypal_upgrade")}
div.payment-submit
button.btn.btn-success.btn-block(
ng-click="submit()"
ng-disabled="processing || !isFormValid(simpleCCForm);"
)
span(ng-show="processing")
i.fa.fa-spinner.fa-spin(aria-hidden="true")
span.sr-only #{translate('processing')}
|  
span(ng-if="paymentMethod.value === 'credit_card'")
| {{ monthlyBilling ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_now")}'}}
span(ng-if="paymentMethod.value !== 'credit_card'") #{translate("upgrade_paypal_btn")}
p.tos-agreement-notice !{translate("by_subscribing_you_agree_to_our_terms_of_service", {}, [{name: 'a', attrs: {href: '/legal#Terms', target:'_blank', rel:'noopener noreferrer'}}])}
div.three-d-secure-container.card.card-highlighted.card-border(ng-show="threeDSecureFlow")
.alert.alert-info.small(aria-live="assertive")
strong #{translate('card_must_be_authenticated_by_3dsecure')}
div.three-d-secure-recurly-container
script(type="text/javascript", nonce=scriptNonce).
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
script(
type="text/ng-template"
id="cvv-tooltip-tpl.html"
)
p !{translate("for_visa_mastercard_and_discover", {}, ['strong', 'strong', 'strong'])}
p !{translate("for_american_express", {}, ['strong', 'strong', 'strong'])}

View file

@ -167,6 +167,8 @@ export default App.controller(
$scope.coupon = {
singleUse: pricing.items.coupon.single_use,
normalPrice: basePrice,
name: pricing.items.coupon.name,
normalPriceWithoutTax: basePrice,
}
if (
pricing.items.coupon.applies_for_months > 0 &&

View file

@ -90,3 +90,41 @@
}
}
}
.price-switch-header {
margin-bottom: @line-height-computed;
h2 {
margin: 0;
}
}
.price-feature-description {
h3 {
margin-top: 0;
}
ul {
padding-left: 10px;
}
li {
list-style-position: inside;
}
}
.price-summary {
.price-summary-line {
display: flex;
justify-content: space-between;
}
.price-summary-total-line {
margin-top: 5px;
font-size: 16px;
}
}
.price-details-spacing {
height: @line-height-computed / 2;
}
.price-cancel-anytime {
font-size: 12px;
}

View file

@ -52,6 +52,7 @@
"recompile_from_scratch": "Recompile from scratch",
"tagline_free": "Perfect for getting started",
"also_provides_free_plan": "__appName__ also provides a free plan -- simply <0>register here</0> to get started.",
"increased_compile_timeout": "Increased compile timeout",
"compile_timeout": "Compile timeout (minutes)",
"collabs_per_proj_single": "__collabcount__ collaborator per project",
"premium_features": "Premium features",
@ -80,6 +81,7 @@
"x_price_per_month": "<0>__price__</0> per month",
"x_price_per_year": "<0>__price__</0> per year",
"x_price_for_first_month": "<0>__price__</0> for your first month",
"x_price_for_y_months": "<0>__price__</0> for your first __discountMonths__ months",
"x_price_for_first_year": "<0>__price__</0> for your first year",
"x_price_per_month_tax": "<0>__total__</0> (__subtotal__ + __tax__ tax) per month",
"x_price_per_year_tax": "<0>__total__</0> (__subtotal__ + __tax__ tax) per year",
@ -89,6 +91,9 @@
"normally_x_price_per_year": "Normally __price__ per year",
"then_x_price_per_month": "Then __price__ per month",
"then_x_price_per_year": "Then __price__ per year",
"total_per_year": "Total per year",
"total_per_month": "Total per month",
"vat": "VAT",
"for_your_first": "for your first",
"sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.",
"template_gallery": "Template Gallery",
@ -255,6 +260,7 @@
" to_reactivate_your_subscription_go_to": "To reactivate your subscription go to",
"subscription_canceled": "Subscription Canceled",
"coupons_not_included": "This does not include your current discounts, which will be applied automatically before your next payment",
"payment_summary": "Payment summary",
"email_already_registered_secondary": "This email is already registered as a secondary email",
"secondary_email_password_reset": "That email is registered as a secondary email. Please enter the primary email for your account.",
"if_registered_email_sent": "If you have an account, we have sent you an email.",
@ -1498,6 +1504,7 @@
"category_operators": "Operators",
"category_relations": "Relations",
"category_misc": "Misc",
"symboly_palette": "Symbol Palette",
"no_symbols_found": "No symbols found",
"learn_more_about_the_symbol_palette": "Learn more about the Symbol Palette and how to use it",
"find_the_symbols_you_need_with_premium": "Find the symbols you need faster with Overleaf Premium",