diff --git a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee index 126a08c881..aca42fd839 100644 --- a/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee +++ b/services/web/app/coffee/Features/Subscription/RecurlyWrapper.coffee @@ -8,6 +8,31 @@ logger = require("logger-sharelatex") module.exports = RecurlyWrapper = apiUrl : "https://api.recurly.com/v2" + createSubscription: (user, subscriptionDetails, recurly_token_id, callback)-> + requestBody = """ + + #{subscriptionDetails.plan_code} + #{subscriptionDetails.currencyCode} + + #{user._id} + #{user.email} + #{user.first_name} + #{user.last_name} + + #{recurly_token_id} + + + + """ + @apiRequest({ + url : "subscriptions" + method : "POST" + body : requestBody + }, (error, response, responseBody) => + return callback(error) if error? + @_parseSubscriptionXml responseBody, callback + ) + apiRequest : (options, callback) -> options.url = @apiUrl + "/" + options.url options.headers = @@ -16,7 +41,7 @@ module.exports = RecurlyWrapper = "Content-Type" : "application/xml; charset=utf-8" request options, (error, response, body) -> unless error? or response.statusCode == 200 or response.statusCode == 201 or response.statusCode == 204 - logger.err err:error, options:options, "error returned from recurly" + logger.err err:error, body:body, options:options, statusCode:response?.statusCode, "error returned from recurly" error = "Recurly API returned with status code: #{response.statusCode}" callback(error, response, body) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee index 7cebfdf535..b7d948a47c 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee @@ -41,7 +41,7 @@ module.exports = SubscriptionController = res.redirect "/user/subscription" else currency = req.query.currency?.toUpperCase() - GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)-> + GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency, countryCode)-> return next(err) if err? if recomendedCurrency? and !currency? currency = recomendedCurrency @@ -56,11 +56,13 @@ module.exports = SubscriptionController = title : "subscribe" plan_code: req.query.planCode currency: currency + countryCode:countryCode plan:plan showStudentPlan: req.query.ssp recurlyConfig: JSON.stringify currency: currency subdomain: Settings.apis.recurly.subdomain + showCouponField:req.query.scf subscriptionFormOptions: JSON.stringify acceptedCards: ['discover', 'mastercard', 'visa'] target : "#subscribeForm" @@ -132,12 +134,14 @@ module.exports = SubscriptionController = createSubscription: (req, res, next)-> SecurityManager.getCurrentUser req, (error, user) -> return callback(error) if error? - subscriptionId = req.body.recurly_token - logger.log subscription_id: subscriptionId, user_id:user._id, "creating subscription" - SubscriptionHandler.createSubscription user, subscriptionId, (err)-> + recurly_token_id = req.body.recurly_token_id + subscriptionDetails = req.body.subscriptionDetails + logger.log recurly_token_id: recurly_token_id, user_id:user._id, subscriptionDetails:subscriptionDetails, "creating subscription" + SubscriptionHandler.createSubscription user, subscriptionDetails, recurly_token_id, (err)-> if err? logger.err err:err, user_id:user._id, "something went wrong creating subscription" - res.redirect "/user/subscription/thank-you" + return res.send 500 + res.send 201 successful_subscription: (req, res)-> SecurityManager.getCurrentUser req, (error, user) => diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee index c3423482ae..4aa3310b89 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionHandler.coffee @@ -10,9 +10,11 @@ DropboxHandler = require("../Dropbox/DropboxHandler") module.exports = - createSubscription: (user, recurlySubscriptionId, callback)-> + createSubscription: (user, subscriptionDetails, recurly_token_id, callback)-> self = @ - RecurlyWrapper.getSubscription recurlySubscriptionId, {recurlyJsResult: true}, (error, recurlySubscription) -> + clientTokenId = "" + RecurlyWrapper.createSubscription user, subscriptionDetails, recurly_token_id, (error, recurlySubscription)-> + console.log recurlySubscription return callback(error) if error? SubscriptionUpdater.syncSubscription recurlySubscription, user._id, (error) -> return callback(error) if error? diff --git a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee index a49627d0cb..5b64b8e6f4 100644 --- a/services/web/app/coffee/infrastructure/GeoIpLookup.coffee +++ b/services/web/app/coffee/infrastructure/GeoIpLookup.coffee @@ -48,4 +48,4 @@ module.exports = GeoIpLookup = countryCode = ipDetails?.country_code?.toUpperCase() currencyCode = currencyMappings[countryCode] || "USD" logger.log ip:ip, currencyCode:currencyCode, ipDetails:ipDetails, "got currencyCode for ip" - callback(err, currencyCode) \ No newline at end of file + callback(err, currencyCode, countryCode) \ No newline at end of file diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade index 8e82cefa25..3d17e9b1a1 100644 --- a/services/web/app/views/layout.jade +++ b/services/web/app/views/layout.jade @@ -14,7 +14,7 @@ html(itemscope, itemtype='http://schema.org/Product') link(rel="icon", href="/favicon.ico") link(rel='stylesheet', href='/stylesheets/style.css?fingerprint='+fingerprint('/stylesheets/style.css')) - link(href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css",rel="stylesheet") + link(href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css",rel="stylesheet") if settings.i18n.subdomainLang each subdomainDetails in settings.i18n.subdomainLang diff --git a/services/web/app/views/subscriptions/new.jade b/services/web/app/views/subscriptions/new.jade index 78ad380d59..53ade84a71 100644 --- a/services/web/app/views/subscriptions/new.jade +++ b/services/web/app/views/subscriptions/new.jade @@ -1,39 +1,170 @@ extends ../layout block scripts + + script(src="https://js.recurly.com/v3/recurly.js") + script(type='text/javascript'). window.recomendedCurrency = '#{currency}' + window.countryCode = '#{countryCode}' window.plan_code = '#{plan_code}' + window.recurlyApiKey = "!{settings.apis.recurly.publicKey}" block content - locals.supressDefaultJs = true script(data-main=jsPath+'main.js', src=jsPath+'libs/require.js', baseurl=jsPath) - script(src=jsPath+'libs/recurly.min.js') + .content.content-alt .container(ng-controller="NewSubscriptionController" ng-cloak) .row.card-group - .col-md-6.col-md-push-3 + .col-md-5.col-md-push-4 .card.card-highlighted .page-header - span.dropdown.changePlanButton.pull-right(ng-cloak) - a.btn.btn-default.dropdown-toggle( - href="#", - data-toggle="dropdown" - ) - | {{currencyCode}} ({{plans[currencyCode]['symbol']}}) - span.caret - ul.dropdown-menu(role="menu") - li(ng-repeat="(currency, value) in plans", dropdown-toggle) - a( - ng-click="changeCurrency(currency)", - ) {{currency}} ({{value['symbol']}}) - h1 #{translate("new_subscription")} - - #subscribeForm(style="min-height: 700px;") #{translate("loading_billing_form")}... - - - .col-md-3.col-md-pull-6 + .row + .col-md-9 + h2 {{planName}} + div !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})} + div #{translate("every")} {{billingCycleType}} + .col-md-3 + div.dropdown.changePlanButton.pull-right(ng-cloak) + a.btn.btn-default.dropdown-toggle( + href="#", + data-toggle="dropdown" + ) + | {{currencyCode}} ({{plans[currencyCode]['symbol']}}) + span.caret + ul.dropdown-menu(role="menu") + li(ng-repeat="(currency, value) in plans", dropdown-toggle) + a( + ng-click="changeCurrency(currency)", + ) {{currency}} ({{value['symbol']}}) + h2.pull-right.totalPrice {{price.currency.symbol}}{{price.next.total}} + .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'") + .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-3 + .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="2015") 2015 + 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', tabindex='1', 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', tabindex='1', onkeyup='', data-recurly="last_name", ng-model="data.last_name", required, placeholder="#{translate('last_name')}") + hr + .row + .col-md-12 + .form-group + label #{translate("billing_address")} + input.form-control(type='text', value='', maxlength='255', tabindex='1', onkeyup='', ng-model="data.address1", placeholder="#{translate('address')}") + .form-group + input.form-control(type='text', value='', maxlength='255', tabindex='1', onkeyup='', ng-model="data.address2", placeholder="#{translate('address')}") + .row + .col-md-7 + .form-group + input.form-control(type='text', value='', maxlength='255', tabindex='1', onkeyup='', data-recurly="city", ng-model="data.city", placeholder="#{translate('city')}") + .col-md-5 + input.form-control(type='text', value='', maxlength='255', tabindex='1', onkeyup='', data-recurly="postal_code", ng-model="data.postal_code", placeholder="#{translate('zip_post_code')}") + .row + .col-md-7 + .form-group + select.form-control(data-recurly="country", ng-model="data.country", ng-change="updateCountry()", required) + mixin countries_options() + .row + .col-md-8 + if showCouponField == 'true' + .form-group + input.form-control(type='text', ng-blur="applyCoupon()", ng-model="data.coupon", placeholder="#{translate('coupon')}") + + + .row + .col-md-6 + .form-group + button.btn.btn-success(ng-click="submit()", ng-disabled="processing") #{translate("upgrade_now")} + + .col-md-3.pricingBreakdown + div Subtotal + div Tax + div + strong Total + .col-md-3 + div {{price.currency.symbol}}{{price.next.subtotal}} + div {{price.currency.symbol}}{{price.next.tax}} + div + strong {{price.currency.symbol}}{{price.next.total}} + + + .col-md-3.col-md-pull-4 if showStudentPlan == 'true' a.btn-primary.btn.plansPageStudentLink( href, @@ -77,16 +208,263 @@ block content script(type="text/javascript"). - Recurly.config(!{recurlyConfig}) - var recurlySubscriptionFormConfig = !{subscriptionFormOptions} - recurlySubscriptionFormConfig.successHandler = function(){ - ga('send', 'event', 'subscription-funnel', 'subscribed') - } - - Recurly.buildSubscriptionForm(recurlySubscriptionFormConfig); - window.ab = [ {step:1, bucket:"red", testName:"button_color"}, {step:1, bucket:"blue", testName:"button_color"} ] + +mixin countries_options() + option(value='', disabled, selected) #{translate("country")} + option(value='-') -------------- + option(value='AF') Afghanistan + option(value='AL') Albania + option(value='DZ') Algeria + option(value='AS') American Samoa + option(value='AD') Andorra + option(value='AO') Angola + option(value='AI') Anguilla + option(value='AQ') Antarctica + option(value='AG') Antigua and Barbuda + option(value='AR') Argentina + option(value='AM') Armenia + option(value='AW') Aruba + option(value='AC') Ascension Island + option(value='AU') Australia + option(value='AT') Austria + option(value='AZ') Azerbaijan + option(value='BS') Bahamas + option(value='BH') Bahrain + option(value='BD') Bangladesh + option(value='BB') Barbados + option(value='BE') Belgium + option(value='BZ') Belize + option(value='BJ') Benin + option(value='BM') Bermuda + option(value='BT') Bhutan + option(value='BO') Bolivia + option(value='BA') Bosnia and Herzegovina + option(value='BW') Botswana + option(value='BV') Bouvet Island + option(value='BR') Brazil + option(value='BQ') British Antarctic Territory + option(value='IO') British Indian Ocean Territory + option(value='VG') British Virgin Islands + option(value='BN') Brunei + option(value='BG') Bulgaria + option(value='BF') Burkina Faso + option(value='BI') Burundi + option(value='KH') Cambodia + option(value='CM') Cameroon + option(value='CA') Canada + option(value='IC') Canary Islands + option(value='CT') Canton and Enderbury Islands + option(value='CV') Cape Verde + option(value='KY') Cayman Islands + option(value='CF') Central African Republic + option(value='EA') Ceuta and Melilla + option(value='TD') Chad + option(value='CL') Chile + option(value='CN') China + option(value='CX') Christmas Island + option(value='CP') Clipperton Island + option(value='CC') Cocos [Keeling] Islands + option(value='CO') Colombia + option(value='KM') Comoros + option(value='CD') Congo [DRC] + option(value='CK') Cook Islands + option(value='CR') Costa Rica + option(value='HR') Croatia + option(value='CU') Cuba + option(value='CY') Cyprus + option(value='CZ') Czech Republic + option(value='DK') Denmark + option(value='DG') Diego Garcia + option(value='DJ') Djibouti + option(value='DM') Dominica + option(value='DO') Dominican Republic + option(value='NQ') Dronning Maud Land + option(value='TL') East Timor + option(value='EC') Ecuador + option(value='EG') Egypt + option(value='SV') El Salvador + option(value='EE') Estonia + option(value='ET') Ethiopia + option(value='FK') Falkland Islands [Islas Malvinas] + option(value='FO') Faroe Islands + option(value='FJ') Fiji + option(value='FI') Finland + option(value='FR') France + option(value='GF') French Guiana + option(value='PF') French Polynesia + option(value='TF') French Southern Territories + option(value='FQ') French Southern and Antarctic Territories + option(value='GA') Gabon + option(value='GM') Gambia + option(value='GE') Georgia + option(value='DE') Germany + option(value='GH') Ghana + option(value='GI') Gibraltar + option(value='GR') Greece + option(value='GL') Greenland + option(value='GD') Grenada + option(value='GP') Guadeloupe + option(value='GU') Guam + option(value='GT') Guatemala + option(value='GG') Guernsey + option(value='GW') Guinea-Bissau + option(value='GY') Guyana + option(value='HT') Haiti + option(value='HM') Heard Island and McDonald Islands + option(value='HN') Honduras + option(value='HK') Hong Kong + option(value='HU') Hungary + option(value='IS') Iceland + option(value='IN') India + option(value='ID') Indonesia + option(value='IE') Ireland + option(value='IM') Isle of Man + option(value='IL') Israel + option(value='IT') Italy + option(value='JM') Jamaica + option(value='JP') Japan + option(value='JE') Jersey + option(value='JT') Johnston Island + option(value='JO') Jordan + option(value='KZ') Kazakhstan + option(value='KE') Kenya + option(value='KI') Kiribati + option(value='KW') Kuwait + option(value='KG') Kyrgyzstan + option(value='LA') Laos + option(value='LV') Latvia + option(value='LS') Lesotho + option(value='LY') Libya + option(value='LI') Liechtenstein + option(value='LT') Lithuania + option(value='LU') Luxembourg + option(value='MO') Macau + option(value='MK') Macedonia [FYROM] + option(value='MG') Madagascar + option(value='MW') Malawi + option(value='MY') Malaysia + option(value='MV') Maldives + option(value='ML') Mali + option(value='MT') Malta + option(value='MH') Marshall Islands + option(value='MQ') Martinique + option(value='MR') Mauritania + option(value='MU') Mauritius + option(value='YT') Mayotte + option(value='FX') Metropolitan France + option(value='MX') Mexico + option(value='FM') Micronesia + option(value='MI') Midway Islands + option(value='MD') Moldova + option(value='MC') Monaco + option(value='MN') Mongolia + option(value='ME') Montenegro + option(value='MS') Montserrat + option(value='MA') Morocco + option(value='MZ') Mozambique + option(value='NA') Namibia + option(value='NR') Nauru + option(value='NP') Nepal + option(value='NL') Netherlands + option(value='AN') Netherlands Antilles + option(value='NT') Neutral Zone + option(value='NC') New Caledonia + option(value='NZ') New Zealand + option(value='NI') Nicaragua + option(value='NE') Niger + option(value='NG') Nigeria + option(value='NU') Niue + option(value='NF') Norfolk Island + option(value='VD') North Vietnam + option(value='MP') Northern Mariana Islands + option(value='NO') Norway + option(value='OM') Oman + option(value='QO') Outlying Oceania + option(value='PC') Pacific Islands Trust Territory + option(value='PK') Pakistan + option(value='PW') Palau + option(value='PS') Palestinian Territories + option(value='PA') Panama + option(value='PZ') Panama Canal Zone + option(value='PY') Paraguay + option(value='YD') People's Democratic Republic of Yemen + option(value='PE') Peru + option(value='PH') Philippines + option(value='PN') Pitcairn Islands + option(value='PL') Poland + option(value='PT') Portugal + option(value='PR') Puerto Rico + option(value='QA') Qatar + option(value='RO') Romania + option(value='RU') Russia + option(value='RW') Rwanda + option(value='RE') Réunion + option(value='BL') Saint Barthélemy + option(value='SH') Saint Helena + option(value='KN') Saint Kitts and Nevis + option(value='LC') Saint Lucia + option(value='MF') Saint Martin + option(value='PM') Saint Pierre and Miquelon + option(value='VC') Saint Vincent and the Grenadines + option(value='WS') Samoa + option(value='SM') San Marino + option(value='SA') Saudi Arabia + option(value='SN') Senegal + option(value='RS') Serbia + option(value='CS') Serbia and Montenegro + option(value='SC') Seychelles + option(value='SL') Sierra Leone + option(value='SG') Singapore + option(value='SK') Slovakia + option(value='SI') Slovenia + option(value='SB') Solomon Islands + option(value='ZA') South Africa + option(value='GS') South Georgia and the South Sandwich Islands + option(value='KR') South Korea + option(value='ES') Spain + option(value='LK') Sri Lanka + option(value='SR') Suriname + option(value='SJ') Svalbard and Jan Mayen + option(value='SZ') Swaziland + option(value='SE') Sweden + option(value='CH') Switzerland + option(value='ST') São Tomé and Príncipe + option(value='TW') Taiwan + option(value='TJ') Tajikistan + option(value='TZ') Tanzania + option(value='TH') Thailand + option(value='TG') Togo + option(value='TK') Tokelau + option(value='TO') Tonga + option(value='TT') Trinidad and Tobago + option(value='TA') Tristan da Cunha + option(value='TN') Tunisia + option(value='TR') Turkey + option(value='TM') Turkmenistan + option(value='TC') Turks and Caicos Islands + option(value='TV') Tuvalu + option(value='UM') U.S. Minor Outlying Islands + option(value='PU') U.S. Miscellaneous Pacific Islands + option(value='VI') U.S. Virgin Islands + option(value='UG') Uganda + option(value='UA') Ukraine + option(value='AE') United Arab Emirates + option(value='GB') United Kingdom + option(value='US') United States + option(value='UY') Uruguay + option(value='UZ') Uzbekistan + option(value='VU') Vanuatu + option(value='VA') Vatican City + option(value='VE') Venezuela + option(value='VN') Vietnam + option(value='WK') Wake Island + option(value='WF') Wallis and Futuna + option(value='EH') Western Sahara + option(value='YE') Yemen + option(value='ZM') Zambia + option(value='AX') Åland Islands diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee index f607cd3435..d7e792bebf 100644 --- a/services/web/public/coffee/main/new-subscription.coffee +++ b/services/web/public/coffee/main/new-subscription.coffee @@ -2,13 +2,107 @@ define [ "base" ], (App)-> - App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager)-> + App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http)-> + throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined" $scope.currencyCode = MultiCurrencyPricing.currencyCode $scope.plans = MultiCurrencyPricing.plans - $scope.changeCurrency = (newCurrency)-> - window.location = "/user/subscription/new?planCode=#{window.plan_code}¤cy=#{newCurrency}" - $scope.switchToStudent = ()-> - window.location = "/user/subscription/new?planCode=student¤cy=#{$scope.currencyCode}" \ No newline at end of file + window.location = "/user/subscription/new?planCode=student¤cy=#{$scope.currencyCode}" + + + $scope.paymentMethod = "credit_card" + + $scope.data = + number: "" + month: "" + year: "" + cvv: "" + first_name: "" + last_name: "" + postal_code: "" + address1 : "" + address2 : "" + city:"" + country:window.countryCode + + + $scope.validation = + correctCardNumber : true + correctExpiry: true + correctCvv:true + + $scope.processing = false + + recurly.configure window.recurlyApiKey + + pricing = recurly.Pricing() + window.pricing = pricing + + pricing.plan(window.plan_code, { quantity: 1 }).address({country: $scope.data.country}).tax({tax_code: 'digital', vat_number: ''}).currency($scope.currencyCode).done() + + pricing.on "change", => + $scope.planName = pricing.items.plan.name + $scope.price = pricing.price + $scope.trialLength = pricing.items.plan.trial.length + $scope.billingCycleType = if pricing.items.plan.period.interval == "months" then "month" else "year" + $scope.$apply() + + $scope.applyCoupon = -> + pricing.coupon($scope.data.coupon).done() + + $scope.changeCurrency = (newCurrency)-> + $scope.currencyCode = newCurrency + pricing.currency(newCurrency).done() + + $scope.validateCardNumber = validateCardNumber = -> + if $scope.data.number?.length != 0 + $scope.validation.correctCardNumber = recurly.validate.cardNumber($scope.data.number) + + $scope.validateExpiry = validateExpiry = -> + 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 = -> + if $scope.data.cvv?.length != 0 + $scope.validation.correctCvv = recurly.validate.cvv($scope.data.cvv) + + $scope.updateCountry = -> + pricing.address({country:$scope.data.country}).done() + + $scope.changePaymentMethod = (paymentMethod)-> + if paymentMethod == "paypal" + $scope.usePaypal = true + else + $scope.usePaypal = false + + completeSubscription = (err, recurly_token_id) -> + $scope.validation.errorFields = {} + if err? + $scope.genericError = err.message + _.each err.fields, (field)-> $scope.validation.errorFields[field] = true + else + postData = + _csrf: window.csrfToken + recurly_token_id:recurly_token_id.id + subscriptionDetails: + currencyCode:pricing.items.currency + plan_code:pricing.items.plan.code + $http.post("/user/subscription/create", postData) + .success (data, status, headers)-> + window.location.href = "/user/subscription/thank-you" + .error (data, status, headers)-> + $scope.processing = false + $scope.genericError = "Something went wrong processing the request" + + $scope.submit = -> + $scope.processing = true + if $scope.paymentMethod == 'paypal' + opts = { description: $scope.planName } + recurly.paypal opts, completeSubscription + else + recurly.token $scope.data, completeSubscription + + + diff --git a/services/web/public/recurly/images/credit_cards/visa_mastercard.png b/services/web/public/recurly/images/credit_cards/visa_mastercard.png new file mode 100644 index 0000000000..feed535881 Binary files /dev/null and b/services/web/public/recurly/images/credit_cards/visa_mastercard.png differ diff --git a/services/web/public/recurly/images/paypal_large.png b/services/web/public/recurly/images/paypal_large.png new file mode 100644 index 0000000000..e8c641ece4 Binary files /dev/null and b/services/web/public/recurly/images/paypal_large.png differ diff --git a/services/web/public/stylesheets/app/plans.less b/services/web/public/stylesheets/app/plans.less index 777a4400c3..c375052394 100644 --- a/services/web/public/stylesheets/app/plans.less +++ b/services/web/public/stylesheets/app/plans.less @@ -96,9 +96,16 @@ margin-top: 20px; } - - - - +input.paymentTypeOption.ng-valid { + margin-top: 15px; +} + +.totalPrice { + padding-top: 15px; +} + +.pricingBreakdown { + text-align: right; +} diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee index cb6c5a663b..624f55ef0d 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee @@ -20,13 +20,13 @@ mockSubscriptions = describe "SubscriptionController sanboxed", -> beforeEach -> - @user = {} + @user = {email:"tom@yahoo.com"} @activeRecurlySubscription = mockSubscriptions["subscription-123-active"] @SecurityManager = getCurrentUser: sinon.stub().callsArgWith(1, null, @user) @SubscriptionHandler = - createSubscription: sinon.stub().callsArgWith(2) + createSubscription: sinon.stub().callsArgWith(3) updateSubscription: sinon.stub().callsArgWith(3) reactivateSubscription: sinon.stub().callsArgWith(1) cancelSubscription: sinon.stub().callsArgWith(1) @@ -258,18 +258,22 @@ describe "SubscriptionController sanboxed", -> describe "createSubscription", -> beforeEach (done)-> @res = - redirect:-> + send:-> done() - sinon.spy @res, "redirect" - @req.body.recurly_token = "1234" + sinon.spy @res, "send" + @subscriptionDetails = + card:"1234" + cvv:"123" + @req.body.recurly_token_id = "1234" + @req.body.subscriptionDetails = @subscriptionDetails @SubscriptionController.createSubscription @req, @res it "should send the user and subscriptionId to the handler", (done)-> - @SubscriptionHandler.createSubscription.calledWith(@user, @req.body.recurly_token).should.equal true + @SubscriptionHandler.createSubscription.calledWith(@user, @subscriptionDetails, @req.body.recurly_token_id).should.equal true done() it "should redurect to the subscription page", (done)-> - @res.redirect.calledWith("/user/subscription/thank-you").should.equal true + @res.send.calledWith(201).should.equal true done() diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionHandlerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionHandlerTests.coffee index 606216fedf..f66a1813d6 100644 --- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionHandlerTests.coffee @@ -42,6 +42,7 @@ describe "Subscription Handler sanboxed", -> cancelSubscription: sinon.stub().callsArgWith(1) reactivateSubscription: sinon.stub().callsArgWith(1) redeemCoupon:sinon.stub().callsArgWith(2) + createSubscription: sinon.stub().callsArgWith(3, null, @activeRecurlySubscription) @DropboxHandler = unlinkAccount:sinon.stub().callsArgWith(1) @@ -71,10 +72,14 @@ describe "Subscription Handler sanboxed", -> describe "createSubscription", -> beforeEach (done) -> - @SubscriptionHandler.createSubscription(@user, @activeRecurlySubscription.uuid, done) + @subscriptionDetails = + cvv:"123" + number:"12345" + @recurly_token_id = "45555666" + @SubscriptionHandler.createSubscription(@user, @subscriptionDetails, @recurly_token_id, done) - it "should get the subscription", (done)-> - @RecurlyWrapper.getSubscription.calledWith(@activeRecurlySubscription.uuid, {recurlyJsResult: true}).should.equal true + it "should create the subscription with the wrapper", (done)-> + @RecurlyWrapper.createSubscription.calledWith(@user, @subscriptionDetails, @recurly_token_id).should.equal true done() it "should sync the subscription to the user", (done)->