mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge branch 'master' of https://github.com/sharelatex/web-sharelatex
This commit is contained in:
commit
9238462fe4
10 changed files with 1005 additions and 214 deletions
|
@ -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
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -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='-') --------------
|
||||
|
|
|
@ -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
|
||||
|
|
539
services/web/public/coffee/directives/creditCards.coffee
Normal file
539
services/web/public/coffee/directives/creditCards.coffee
Normal 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
|
||||
|
||||
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ define [
|
|||
"directives/onEnter"
|
||||
"directives/selectAll"
|
||||
"directives/maxHeight"
|
||||
"directives/creditCards"
|
||||
"services/queued-http"
|
||||
"filters/formatDate"
|
||||
"__MAIN_CLIENTSIDE_INCLUDES__"
|
||||
|
|
|
@ -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¤cy=#{$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
|
||||
|
|
|
@ -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;
|
||||
|
|
74
services/web/public/stylesheets/app/subscription.less
Normal file
74
services/web/public/stylesheets/app/subscription.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
||||
|
|
Loading…
Reference in a new issue