This commit is contained in:
Henry Oswald 2016-08-24 16:34:47 +01:00
commit 9238462fe4
10 changed files with 1005 additions and 214 deletions

View file

@ -61,11 +61,17 @@ module.exports = (app, webRouter, apiRouter)->
webRouter.use (req, res, next)->
cdnBlocked = req.query.nocdn == 'true' or req.session.cdnBlocked
if cdnBlocked and !req.session.cdnBlocked?
logger.log user_id:req?.session?.user?._id, ip:req?.ip, "cdnBlocked for user, not using it and turning it off for future requets"
req.session.cdnBlocked = true
isDark = req.headers?.host?.slice(0,4)?.toLowerCase() == "dark"
isSmoke = req.headers?.host?.slice(0,5)?.toLowerCase() == "smoke"
isLive = !isDark and !isSmoke
if cdnAvailable and isLive
if cdnAvailable and isLive and !cdnBlocked
staticFilesBase = Settings.cdn?.web?.host
else if darkCdnAvailable and isDark
staticFilesBase = Settings.cdn?.web?.darkHost

View file

@ -8,6 +8,7 @@ html(itemscope, itemtype='http://schema.org/Product')
window.similarproducts = true
style [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {display: none !important; display: none; }
-if (typeof(gaExperiments) != "undefined")
|!{gaExperiments}
@ -52,7 +53,15 @@ html(itemscope, itemtype='http://schema.org/Product')
block scripts
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(type="text/javascript").
var noCdnKey = "nocdn=true"
var cdnBlocked = typeof jQuery === 'undefined'
var noCdnAlreadyInUrl = window.location.href.indexOf(noCdnKey) != -1 //prevent loops
if (cdnBlocked && !noCdnAlreadyInUrl) {
window.location.search += '&'+noCdnKey;
}
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
script.
window.sharelatex = {
siteUrl: '#{settings.siteUrl}',

View file

@ -51,226 +51,341 @@ block content
div(ng-if="normalPrice")
span.small Normally {{price.currency.symbol}}{{normalPrice}}
.row
.col-md-12
form(ng-show="planName")
.row
.col-md-12
.form-group
.row
.col-md-6
label.radio-inline
input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod")
i.fa.fa-cc-mastercard.fa-3x
span  
i.fa.fa-cc-visa.fa-3x
.col-md-6
label.radio-inline
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod")
i.fa.fa-cc-paypal.fa-3x
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
span(ng-hide="paymentMethod == 'paypal'")
div(sixpack-switch="subscription-form")
.col-md-12(sixpack-default)
form(ng-show="planName",novalidate)
.row
.col-md-12
.form-group
div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
.row
.col-md-6
label.radio-inline
input.paymentTypeOption(type="radio",value="credit_card", ng-model="paymentMethod.value")
i.fa.fa-cc-mastercard.fa-3x
span  
i.fa.fa-cc-visa.fa-3x
.col-md-6
label.radio-inline
input.paymentTypeOption(type="radio", value="paypal", ng-model="paymentMethod.value")
i.fa.fa-cc-paypal.fa-3x
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
span(ng-hide="paymentMethod.value == 'paypal'")
.row
.col-md-12
.form-group
div.alert.alert-warning.small(ng-hide="validation.correctCvv") #{translate("invalid")} CVV
div.alert.alert-warning.small(ng-hide="validation.correctCardNumber") #{translate("invalid")} #{translate("credit_card_number")}
.row
.col-md-6
.form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
.col-md-3
.form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
.row
.col-md-12
div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
.row
.col-md-3
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
option(value="", disabled, selected) Month
option(value="01") 01
option(value="02") 02
option(value="03") 03
option(value="04") 04
option(value="05") 05
option(value="06") 06
option(value="07") 07
option(value="08") 08
option(value="09") 09
option(value="10") 10
option(value="11") 11
option(value="12") 12
.col-md-4
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
option(value="", disabled, selected) Year
option(value="2016") 2016
option(value="2017") 2017
option(value="2018") 2018
option(value="2019") 2019
option(value="2020") 2020
option(value="2021") 2021
option(value="2022") 2022
option(value="2023") 2023
option(value="2024") 2024
option(value="2025") 2025
option(value="2026") 2026
.row
.col-md-6
.form-group(ng-class="validation.number == false || validation.errorFields.number ? 'has-error' : ''")
input.form-control(ng-model='data.number', ng-blur="validateCardNumber()", placeholder="#{translate('credit_card_number')}")
.col-md-3
.form-group(ng-class="validation.correctCvv == false || validation.errorFields.cvv ? 'has-error' : ''")
input.form-control(ng-model='data.cvv', ng-blur="validateCvv()", placeholder="CVV")
.form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
.col-md-6
.form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
hr
.row
.col-md-12
div.alert.alert-warning.small(ng-hide="validation.correctExpiry") #{translate("invalid")} #{translate("expiry")}
.form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
label #{translate("billing_address")}
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
.form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
.form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
.row
.col-md-3
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.month ? 'has-error' : ''")
select.form-control(data-recurly='month', ng-change="validateExpiry()", ng-model='data.month')
option(value="", disabled, selected) Month
option(value="01") 01
option(value="02") 02
option(value="03") 03
option(value="04") 04
option(value="05") 05
option(value="06") 06
option(value="07") 07
option(value="08") 08
option(value="09") 09
option(value="10") 10
option(value="11") 11
option(value="12") 12
.col-md-4
.form-group(ng-class="validation.correctExpiry == false || validation.errorFields.year ? 'has-error' : ''")
select.form-control(data-recurly='year', ng-change="validateExpiry()", ng-model='data.year')
option(value="", disabled, selected) Year
option(value="2016") 2016
option(value="2017") 2017
option(value="2018") 2018
option(value="2019") 2019
option(value="2020") 2020
option(value="2021") 2021
option(value="2022") 2022
option(value="2023") 2023
option(value="2024") 2024
option(value="2025") 2025
option(value="2026") 2026
.row
.col-md-6
.form-group(ng-class="validation.errorFields.first_name ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', , onkeyup='', data-recurly="first_name", ng-model="data.first_name", required, placeholder="#{translate('first_name')}")
.col-md-6
.form-group(ng-class="validation.errorFields.last_name ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}")
hr
.row
.col-md-12
.form-group(ng-class="validation.errorFields.address1 ? 'has-error' : ''")
label #{translate("billing_address")}
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}")
.form-group(ng-class="validation.errorFields.address2 ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}")
.form-group(ng-class="validation.errorFields.state ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', ng-model="data.state", placeholder="#{translate('state')}")
.row
.col-md-7
.form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
.col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
.row
.col-md-7
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
mixin countries_options()
.row
.col-md-8
if (showCouponField)
.col-md-7
.form-group(ng-class="validation.errorFields.city ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}")
.col-md-5(ng-class="validation.errorFields.postal_code ? 'has-error' : ''")
input.form-control(type='text', value='', maxlength='255', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}")
.row
.col-md-7
.form-group(ng-class="validation.errorFields.country ? 'has-error' : ''")
select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required)
mixin countries_options()
.row
.col-md-8
if (showCouponField)
.form-group
input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
.row
.col-md-8
if (showVatField)
.form-group
input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
.row
.col-xs-7
.form-group
input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}")
.row
.col-md-8
button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
.col-xs-3.pricingBreakdown
div(ng-if="price.next.subtotal != price.next.total") Subtotal
div(ng-if="price.next.tax!='0.00'") Tax
div
strong Total
.col-xs-2
div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
div.pull-right
strong {{price.currency.symbol}}{{price.next.total}}
.col-md-12(sixpack-when="simple")
form(
ng-if="planName"
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
span  
i.fa.fa-cc-visa.fa-2x
span  
i.fa.fa-cc-amex.fa-2x
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
.alert.alert-warning.small(ng-show="genericError")
strong {{genericError}}
div(ng-if="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 {{ simpleCCForm.firstName.$error.required ? 'This field is required' : '' }}
.col-xs-6
.form-group(for="last-name",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 {{ simpleCCForm.lastName.$error.required ? 'This field is required' : '' }}
.form-group(ng-class="validation.correctCardNumber == false || validation.errorFields.number || inputHasError(simpleCCForm.ccNumber) ? 'has-error' : ''")
label(for="card-no") #{translate("credit_card_number")}
input#card-no.form-control(
type="text"
ng-model="data.number"
name="ccNumber"
ng-focus="validation.correctCardNumber = true; validation.errorFields.number = false;"
ng-blur="validateCardNumber();"
required
cc-format-card-number
)
span.input-feedback-message {{ simpleCCForm.ccNumber.$error.required ? 'This field is required' : 'Please re-check the card number' }}
.row
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctExpiry == false || validation.errorFields.expiry || inputHasError(simpleCCForm.expiry) ? 'has-error' : ''")
label #{translate("expiry")}
input.form-control(
type="text"
ng-model="data.mmYY"
name="expiry"
placeholder="MM / YY"
ng-focus="validation.correctExpiry = true; validation.errorFields.expiry = false;"
ng-blur="updateExpiry(); validateExpiry()"
required
cc-format-expiry
)
span.input-feedback-message {{ simpleCCForm.expiry.$error.required ? 'This field is required' : 'Please re-check the expiry date' }}
.col-xs-6
.form-group.has-feedback(ng-class="validation.correctCvv == false || validation.errorFields.cvv || inputHasError(simpleCCForm.cvv) ? 'has-error' : ''")
label #{translate("security_code")}
input.form-control(
type="text"
ng-model="data.cvv"
ng-focus="validation.correctCvv = true; validation.errorFields.cvv = false;"
ng-blur="validateCvv()"
name="cvv"
required
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"
) ?
span.input-feedback-message {{ simpleCCForm.cvv.$error.required ? 'This field is required' : 'Please re-check the security code' }}
div
.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()"
required
)
mixin countries_options()
span.input-feedback-message {{ simpleCCForm.country.$error.required ? 'This field is required' : '' }}
if (showVatField)
.form-group
input.form-control(type='text', ng-blur="applyVatNumber()", ng-model="data.vat_number", placeholder="#{translate('vat_number')}")
.row
.col-xs-7
.form-group
button.btn.btn-success(ng-click="submit()", ng-disabled="processing", sixpack-convert="payment-left-menu-bottom") #{translate("upgrade_now")}
.col-xs-3.pricingBreakdown
div(ng-if="price.next.subtotal != price.next.total") Subtotal
div(ng-if="price.next.tax!='0.00'") Tax
div
strong Total
.col-xs-2
div(ng-if="price.next.subtotal != price.next.total").pull-right {{price.currency.symbol}}{{price.next.subtotal}}
div(ng-if="price.next.tax!='0.00'").pull-right {{price.currency.symbol}}{{price.next.tax}}
div.pull-right
strong {{price.currency.symbol}}{{price.next.total}}
label(for="vat-no") #{translate('vat_number')}
input#vat-no.form-control(
type="text"
ng-blur="applyVatNumber()"
ng-model="data.vat_number"
)
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.price-breakdown(ng-if="price.next.tax !== '0.00'")
hr.thin
span Total:
strong {{price.currency.symbol}}{{price.next.total}}
span ({{price.currency.symbol}}{{price.next.subtotal}} + {{price.currency.symbol}}{{price.next.tax}} tax)
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
hr.thin
span(sixpack-switch="payment-left-menu-bottom")
div.payment-submit
button.btn.btn-success.btn-block(
ng-click="submit()"
ng-disabled="processing || !isFormValid(simpleCCForm);"
sixpack-convert="payment-left-menu-bottom"
)
span(ng-show="processing")
i.fa.fa-spinner.fa-spin
|  
| {{ paymentMethod.value === 'credit_card' ? '#{translate("upgrade_cc_btn")}' : '#{translate("upgrade_paypal_btn")}' }}
.col-md-3.col-md-pull-4(sixpack-default)
if showStudentPlan == 'true'
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.card.card-first
.paymentPageFeatures
h3 #{translate("unlimited_projects")}
p #{translate("create_unlimited_projects")}
.col-md-3.col-md-pull-4
if showStudentPlan == 'true'
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.card.card-first
.paymentPageFeatures
h3 #{translate("unlimited_projects")}
p #{translate("create_unlimited_projects")}
h3
if plan.features.collaborators == -1
- var collaboratorCount = 'Unlimited'
else
- var collaboratorCount = plan.features.collaborators
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
h3
if plan.features.collaborators == -1
- var collaboratorCount = 'Unlimited'
else
- var collaboratorCount = plan.features.collaborators
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
p #{translate("work_on_single_version")}. #{translate("view_collab_edits")} in real time.
h3 #{translate("full_doc_history")}
p #{translate("see_what_has_been")}
span.added #{translate("added")}
| #{translate("and")}
span.removed #{translate("removed")}.
| #{translate("restore_to_any_older_version")}.
h3 #{translate("sync_to_dropbox")}
p
| #{translate("acces_work_from_anywhere")}.
| #{translate("work_offline_and_sync_with_dropbox")}.
hr
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
hr
span                  
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
.col-md-3.col-md-pull-4(sixpack-when="bolder")
if showStudentPlan == 'true'
a.btn-primary.btn.plansPageStudentLink(
href,
ng-click="switchToStudent()"
) #{translate("half_price_student")}
.card.card-first
.paymentPageFeatures
.page-header
h2 #{translate("features")}
h3
i.fa.fa-check  
| #{translate("unlimited_projects")}
h3 #{translate("full_doc_history")}
p #{translate("see_what_has_been")}
span.added #{translate("added")}
| #{translate("and")}
span.removed #{translate("removed")}.
| #{translate("restore_to_any_older_version")}.
h3
i.fa.fa-check  
if plan.features.collaborators == -1
- var collaboratorCount = 'Unlimited'
else
- var collaboratorCount = plan.features.collaborators
| #{translate("collabs_per_proj", {collabcount:collaboratorCount})}
h3 #{translate("sync_to_dropbox")}
p
| #{translate("acces_work_from_anywhere")}.
| #{translate("work_offline_and_sync_with_dropbox")}.
h3
i.fa.fa-check  
| #{translate("full_doc_history")}
hr
h3
i.fa.fa-check  
| #{translate("sync_to_dropbox")}
h3
i.fa.fa-check  
| #{translate("sync_to_github")}
h3
i.fa.fa-check  
| #{translate("Compile Larger Projects")}
hr
h2.text-center 30 Day Guarantee
hr
span                  
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
p.small.text-center We're confident that you'll love ShareLaTeX, but if not you can cancel anytime. We'll give you your money back, no questions asked, if you let us know within 30 days.
hr
span                  
a(href="https://www.positivessl.com" style="font-family: arial; font-size: 10px; color: #212121; text-decoration: none;")
img(src="https://www.positivessl.com/images-new/PositiveSSL_tl_trans.png" alt="SSL Certificate" title="SSL Certificate" border="0")
div(style="font-family: arial;font-weight:bold;font-size:15px;color:#86BEE0;")
a(href="https://www.positivessl.com" style="color:#86BEE0; text-decoration: none;")
script(type="text/javascript").
ga('send', 'event', 'pageview', 'payment_form', "#{plan_code}")
script(
type="text/ng-template"
id="cvv-tooltip-tpl.html"
)
p For #[strong Visa, MasterCard and Discover], the #[strong 3 digits] on the #[strong back] of your card.
p For #[strong American Express], the #[strong 4 digits] on the #[strong front] of your card.
mixin countries_options()
option(value='', disabled, selected) #{translate("country")}
option(value='-') --------------

View file

@ -116,7 +116,7 @@ module.exports = settings =
# cdn:
# web:
# host:"http://cdn.sharelatex.dev:3000"
# host:"http://nowhere.sharelatex.dev"
# darkHost:"http://cdn.sharelatex.dev:3000"
# Where your instance of ShareLaTeX can be found publically. Used in emails

View file

@ -0,0 +1,539 @@
define [
"base"
], (App) ->
App.factory 'ccUtils', () ->
defaultFormat = /(\d{1,4})/g;
defaultInputFormat = /(?:^|\s)(\d{4})$/
cards = [
# Credit cards
{
type: 'visa'
patterns: [4]
format: defaultFormat
length: [13, 16]
cvcLength: [3]
luhn: true
}
{
type: 'mastercard'
patterns: [
51, 52, 53, 54, 55,
22, 23, 24, 25, 26, 27
]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'amex'
patterns: [34, 37]
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
length: [15]
cvcLength: [3..4]
luhn: true
}
{
type: 'dinersclub'
patterns: [30, 36, 38, 39]
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
length: [14]
cvcLength: [3]
luhn: true
}
{
type: 'discover'
patterns: [60, 64, 65, 622]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'unionpay'
patterns: [62, 88]
format: defaultFormat
length: [16..19]
cvcLength: [3]
luhn: false
}
{
type: 'jcb'
patterns: [35]
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
]
cardFromNumber = (num) ->
num = (num + '').replace(/\D/g, "")
for card in cards
for pattern in card.patterns
p = pattern + ""
return card if num.substr(0, p.length) == p
cardFromType = (type) ->
return card for card in cards when card.type is type
cardType = (num) ->
return null unless num
cardFromNumber(num)?.type or null
formatCardNumber = (num) ->
num = num.replace(/\D/g, '')
card = cardFromNumber(num)
return num unless card
upperLength = card.length[card.length.length - 1]
num = num[0...upperLength]
if card.format.global
num.match(card.format)?.join(' ')
else
groups = card.format.exec(num)
return unless groups?
groups.shift()
groups = $.grep(groups, (n) -> n) # Filter empty groups
groups.join(' ')
formatExpiry = (expiry) ->
parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
return '' unless parts
mon = parts[1] || ''
sep = parts[2] || ''
year = parts[3] || ''
if year.length > 0
sep = ' / '
else if sep is ' /'
mon = mon.substring(0, 1)
sep = ''
else if mon.length == 2 or sep.length > 0
sep = ' / '
else if mon.length == 1 and mon not in ['0', '1']
mon = "0#{mon}"
sep = ' / '
return mon + sep + year
parseExpiry = (value = "") ->
[month, year] = value.split(/[\s\/]+/, 2)
# Allow for year shortcut
if year?.length is 2 and /^\d+$/.test(year)
prefix = (new Date).getFullYear()
prefix = prefix.toString()[0..1]
year = prefix + year
month = parseInt(month, 10)
year = parseInt(year, 10)
return unless !isNaN(month) and !isNaN(year)
month: month, year: year
return {
fromNumber: cardFromNumber
fromType: cardFromType
cardType: cardType
formatExpiry: formatExpiry
formatCardNumber: formatCardNumber
defaultFormat: defaultFormat
defaultInputFormat: defaultInputFormat
parseExpiry: parseExpiry
}
App.factory 'ccFormat', (ccUtils, $filter) ->
hasTextSelected = ($target) ->
# If some text is selected
return true if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt $target.prop('selectionEnd')
# If some text is selected in IE
if document?.selection?.createRange?
return true if document.selection.createRange().text
false
safeVal = (value, $target) ->
try
cursor = $target.prop('selectionStart')
catch error
cursor = null
last = $target.val()
$target.val(value)
if cursor != null && $target.is(":focus")
cursor = value.length if cursor is last.length
# This hack looks for scenarios where we are changing an input's value such
# that "X| " is replaced with " |X" (where "|" is the cursor). In those
# scenarios, we want " X|".
#
# For example:
# 1. Input field has value "4444| "
# 2. User types "1"
# 3. Input field has value "44441| "
# 4. Reformatter changes it to "4444 |1"
# 5. By incrementing the cursor, we make it "4444 1|"
#
# This is awful, and ideally doesn't go here, but given the current design
# of the system there does not appear to be a better solution.
#
# Note that we can't just detect when the cursor-1 is " ", because that
# would incorrectly increment the cursor when backspacing, e.g. pressing
# backspace in this scenario: "4444 1|234 5".
if last != value
prevPair = last[cursor-1..cursor]
currPair = value[cursor-1..cursor]
digit = value[cursor]
cursor = cursor + 1 if /\d/.test(digit) and
prevPair == "#{digit} " and currPair == " #{digit}"
$target.prop('selectionStart', cursor)
$target.prop('selectionEnd', cursor)
# Replace Full-Width Chars
replaceFullWidthChars = (str = '') ->
fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
halfWidth = '0123456789'
value = ''
chars = str.split('')
# Avoid using reserved word `char`
for chr in chars
idx = fullWidth.indexOf(chr)
chr = halfWidth[idx] if idx > -1
value += chr
value
# Format Numeric
reFormatNumeric = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')
safeVal(value, $target)
# Format Card Number
reFormatCardNumber = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = ccUtils.formatCardNumber(value)
safeVal(value, $target)
formatCardNumber = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
value = $target.val()
card = ccUtils.fromNumber(value + digit)
length = (value.replace(/\D/g, '') + digit).length
upperLength = 16
upperLength = card.length[card.length.length - 1] if card
return if length >= upperLength
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
if card && card.type is 'amex'
# AMEX cards are formatted differently
re = /^(\d{4}|\d{4}\s\d{6})$/
else
re = /(?:^|\s)(\d{4})$/
# If '4242' + 4
if re.test(value)
e.preventDefault()
setTimeout -> $target.val(value + ' ' + digit)
# If '424' + 2
else if re.test(value + digit)
e.preventDefault()
setTimeout -> $target.val(value + digit + ' ')
formatBackCardNumber = (e) ->
$target = $(e.currentTarget)
value = $target.val()
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
# Remove the digit + trailing space
if /\d\s$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d\s$/, ''))
# Remove digit if ends in space + digit
else if /\s\d?$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d$/, ''))
getFormattedCardNumber = (num) ->
num = num.replace(/\D/g, '')
card = ccUtils.fromNumber(num)
return num unless card
upperLength = card.length[card.length.length - 1]
num = num[0...upperLength]
if card.format.global
num.match(card.format)?.join(' ')
else
groups = card.format.exec(num)
return unless groups?
groups.shift()
groups = $.grep(groups, (n) -> n) # Filter empty groups
groups.join(' ')
parseCardNumber = (value) ->
if value? then value.replace(/\s/g, '') else value
# Format Expiry
reFormatExpiry = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = ccUtils.formatExpiry(value)
safeVal(value, $target)
formatExpiry = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
val = $target.val() + digit
if /^\d$/.test(val) and val not in ['0', '1']
e.preventDefault()
setTimeout -> $target.val("0#{val} / ")
else if /^\d\d$/.test(val)
e.preventDefault()
setTimeout ->
# Split for months where we have the second digit > 2 (past 12) and turn
# that into (m1)(m2) => 0(m1) / (m2)
m1 = parseInt(val[0], 10)
m2 = parseInt(val[1], 10)
if m2 > 2 and m1 != 0
$target.val("0#{m1} / #{m2}")
else
$target.val("#{val} / ")
formatForwardExpiry = (e) ->
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
$target = $(e.currentTarget)
val = $target.val()
if /^\d\d$/.test(val)
$target.val("#{val} / ")
formatForwardSlash = (e) ->
which = String.fromCharCode(e.which)
return unless which is '/' or which is ' '
$target = $(e.currentTarget)
val = $target.val()
if /^\d$/.test(val) and val isnt '0'
$target.val("0#{val} / ")
formatBackExpiry = (e) ->
$target = $(e.currentTarget)
value = $target.val()
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if $target.prop('selectionStart')? and
$target.prop('selectionStart') isnt value.length
# Remove the trailing space + last digit
if /\d\s\/\s$/.test(value)
e.preventDefault()
setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
parseExpiry = (value) ->
if value?
dateAsObj = ccUtils.parseExpiry(value)
return unless dateAsObj?
expiry = new Date dateAsObj.year, dateAsObj.month - 1
return $filter('date')(expiry, 'MM/yyyy')
# Format CVC
reFormatCVC = (e) ->
$target = $(e.currentTarget)
setTimeout ->
value = $target.val()
value = replaceFullWidthChars(value)
value = value.replace(/\D/g, '')[0...4]
safeVal(value, $target)
# Restrictions
restrictNumeric = (e) ->
# Key event is for a browser shortcut
return true if e.metaKey or e.ctrlKey
# If keycode is a space
return false if e.which is 32
# If keycode is a special char (WebKit)
return true if e.which is 0
# If char is a special char (Firefox)
return true if e.which < 33
input = String.fromCharCode(e.which)
# Char is a number or a space
!!/[\d\s]/.test(input)
restrictCardNumber = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
# Restrict number of digits
value = ($target.val() + digit).replace(/\D/g, '')
card = ccUtils.fromNumber(value)
if card
value.length <= card.length[card.length.length - 1]
else
# All other cards are 16 digits long
value.length <= 16
restrictExpiry = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
value = $target.val() + digit
value = value.replace(/\D/g, '')
return false if value.length > 6
restrictCVC = (e) ->
$target = $(e.currentTarget)
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected($target)
val = $target.val() + digit
val.length <= 4
setCardType = (e) ->
$target = $(e.currentTarget)
val = $target.val()
cardType = ccUtils.cardType(val) or 'unknown'
unless $target.hasClass(cardType)
allTypes = (card.type for card in cards)
$target.removeClass('unknown')
$target.removeClass(allTypes.join(' '))
$target.addClass(cardType)
$target.toggleClass('identified', cardType isnt 'unknown')
$target.trigger('payment.cardType', cardType)
return {
hasTextSelected
replaceFullWidthChars
reFormatNumeric
reFormatCardNumber
formatCardNumber
formatBackCardNumber
getFormattedCardNumber
parseCardNumber
reFormatExpiry
formatExpiry
formatForwardExpiry
formatForwardSlash
formatBackExpiry
parseExpiry
reFormatCVC
restrictNumeric
restrictCardNumber
restrictExpiry
restrictCVC
setCardType
}
App.directive "ccFormatExpiry", (ccFormat) ->
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictExpiry
el.on "keypress", ccFormat.formatExpiry
el.on "keypress", ccFormat.formatForwardSlash
el.on "keypress", ccFormat.formatForwardExpiry
el.on "keydown", ccFormat.formatBackExpiry
el.on "change", ccFormat.reFormatExpiry
el.on "input", ccFormat.reFormatExpiry
el.on "paste", ccFormat.reFormatExpiry
ngModel.$parsers.push ccFormat.parseExpiry
ngModel.$formatters.push ccFormat.parseExpiry
App.directive "ccFormatCardNumber", (ccFormat) ->
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictCardNumber
el.on "keypress", ccFormat.formatCardNumber
el.on "keydown", ccFormat.formatBackCardNumber
el.on "paste", ccFormat.reFormatCardNumber
ngModel.$parsers.push ccFormat.parseCardNumber
ngModel.$formatters.push ccFormat.getFormattedCardNumber
App.directive "ccFormatSecCode", (ccFormat) ->
restrict: "A"
require: "ngModel"
link: (scope, el, attrs, ngModel) ->
el.on "keypress", ccFormat.restrictNumeric
el.on "keypress", ccFormat.restrictCVC
el.on "paste", ccFormat.reFormatCVC
el.on "change", ccFormat.reFormatCVC
el.on "input", ccFormat.reFormatCVC

View file

@ -26,6 +26,7 @@ define [
"directives/onEnter"
"directives/selectAll"
"directives/maxHeight"
"directives/creditCards"
"services/queued-http"
"filters/formatDate"
"__MAIN_CLIENTSIDE_INCLUDES__"

View file

@ -1,8 +1,9 @@
define [
"base"
"base",
"directives/creditCards"
], (App)->
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking)->
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
$scope.currencyCode = MultiCurrencyPricing.currencyCode
@ -11,8 +12,10 @@ define [
$scope.switchToStudent = ()->
window.location = "/user/subscription/new?planCode=student_free_trial_7_days&currency=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
event_tracking.sendMB "subscription-form", { plan : window.plan_code }
$scope.paymentMethod = "credit_card"
$scope.paymentMethod =
value: "credit_card"
$scope.data =
number: ""
@ -28,12 +31,12 @@ define [
city:""
country:window.countryCode
coupon: window.couponCode
mmYY: ""
$scope.validation =
correctCardNumber : true
correctExpiry: true
correctCvv:true
correctCvv: true
$scope.processing = false
@ -51,12 +54,11 @@ define [
.done()
pricing.on "change", =>
event_tracking.sendMB "subscription-form", { plan : pricing.items.plan.code }
$scope.planName = pricing.items.plan.name
$scope.price = pricing.price
$scope.trialLength = pricing.items.plan.trial?.length
$scope.monthlyBilling = pricing.items.plan.period.length == 1
if pricing.items?.coupon?.discount?.type == "percent"
basePrice = parseInt(pricing.price.base.plan.unit)
$scope.normalPrice = basePrice
@ -74,35 +76,58 @@ define [
$scope.applyVatNumber = ->
pricing.tax({tax_code: 'digital', vat_number: $scope.data.vat_number}).done()
$scope.changeCurrency = (newCurrency)->
$scope.currencyCode = newCurrency
pricing.currency(newCurrency).done()
$scope.updateExpiry = () ->
parsedDateObj = ccUtils.parseExpiry $scope.data.mmYY
if parsedDateObj?
$scope.data.month = parsedDateObj.month
$scope.data.year = parsedDateObj.year
$scope.validateCardNumber = validateCardNumber = ->
$scope.validation.errorFields = {}
if $scope.data.number?.length != 0
$scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number)
$scope.validateExpiry = validateExpiry = ->
$scope.validation.errorFields = {}
if $scope.data.month?.length != 0 and $scope.data.year?.length != 0
$scope.validation.correctExpiry = recurly.validate.expiry($scope.data.month, $scope.data.year)
$scope.validateCvv = validateCvv = ->
$scope.validation.errorFields = {}
if $scope.data.cvv?.length != 0
$scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv)
$scope.inputHasError = inputHasError = (formItem) ->
if !formItem?
return false
return (formItem.$touched && formItem.$invalid)
$scope.isFormValid = isFormValid = (form) ->
if $scope.paymentMethod.value == 'paypal'
return $scope.data.country != ""
else
return (form.$valid and
$scope.validation.correctCardNumber and
$scope.validation.correctExpiry and
$scope.validation.correctCvv)
$scope.updateCountry = ->
pricing.address({country:$scope.data.country}).done()
$scope.changePaymentMethod = (paymentMethod)->
if paymentMethod == "paypal"
$scope.usePaypal = true
else
$scope.usePaypal = false
$scope.setPaymentMethod = setPaymentMethod = (method) ->
$scope.paymentMethod.value = method;
$scope.validation.errorFields = {}
$scope.genericError = ""
completeSubscription = (err, recurly_token_id) ->
$scope.validation.errorFields = {}
if err?
event_tracking.sendMB "subscription-error", err
# We may or may not be in a digest loop here depending on
# whether recurly could do validation locally, so do it async
$scope.$evalAsync () ->
@ -117,7 +142,7 @@ define [
currencyCode:pricing.items.currency
plan_code:pricing.items.plan.code
coupon_code:pricing.items?.coupon?.code || ""
isPaypal: $scope.paymentMethod == 'paypal'
isPaypal: $scope.paymentMethod.value == 'paypal'
address:
address1: $scope.data.address1
address2: $scope.data.address2
@ -132,6 +157,8 @@ define [
isPaypal : postData.subscriptionDetails.isPaypal
}
sixpack.convert "subscription-form"
$http.post("/user/subscription/create", postData)
.success (data, status, headers)->
sixpack.convert "in-editor-free-trial-plan", pricing.items.plan.code, (err)->
@ -143,7 +170,7 @@ define [
$scope.submit = ->
$scope.processing = true
if $scope.paymentMethod == 'paypal'
if $scope.paymentMethod.value == 'paypal'
opts = { description: $scope.planName }
recurly.paypal opts, completeSubscription
else

View file

@ -89,8 +89,27 @@
.small {
font-size: 12px;
}
}
.feature {
margin-top: (@line-height-computed / 2);
margin-bottom: (@line-height-computed / 1.5);
}
.features-check,
.features-copy {
display: inline-block;
width: 12%;
line-height: 1.4;
vertical-align: top;
}
.features-copy {
width: 88%;
}
.plansPageStudentLink {
margin-left: 20px;
margin-top: 20px;

View file

@ -0,0 +1,74 @@
.form-helper {
display: inline-block;
width: 1.3em;
height: 1.3em;
line-height: 1.3;
vertical-align: initial;
background-color: @gray;
color: #FFF;
font-weight: bolder;
border-radius: 50%;
&:hover,
&:focus {
color: #FFF;
text-decoration: none;
}
}
.price-breakdown {
text-align: center;
margin-bottom: -10px;
}
.input-feedback-message {
display: none;
font-size: 0.8em;
.has-error & {
display: inline-block;
}
}
.payment-submit {
padding-top: (@line-height-computed / 2);
}
.payment-method-toggle {
margin-bottom: (@line-height-computed / 2);
&-switch {
display: inline-block;
width: 50%;
text-align: center;
border: solid 1px @gray-lighter;
border-radius: @border-radius-large 0 0 @border-radius-large;
padding: (@line-height-computed / 2);
color: @gray;
&:hover,
&:focus {
color: @gray;
text-decoration: none;
}
&:hover {
color: @gray-dark;
}
& + & {
border-left-width: 0;
border-radius: 0 @border-radius-large @border-radius-large 0;
}
&-selected {
color: @link-color;
box-shadow: inset 0 -2px 0 0;
&:hover,
&:focus {
color: @link-color;
}
}
}
}

View file

@ -75,6 +75,7 @@
@import "app/wiki.less";
@import "app/translations.less";
@import "app/contact-us.less";
@import "app/subscription.less";
@import "app/sprites.less";
@import "app/invite.less";