diff --git a/admin/app/admin/jam_track_right.rb b/admin/app/admin/jam_track_right.rb index 5e7c3687b..3c591234a 100644 --- a/admin/app/admin/jam_track_right.rb +++ b/admin/app/admin/jam_track_right.rb @@ -64,7 +64,7 @@ ActiveAdmin.register JamRuby::JamTrackRight, :as => 'JamTrackRights' do begin client.find_or_create_account(user, billing_info) - client.place_order(user, jam_track) + client.place_order(user, jam_track, nil) rescue RecurlyClientError=>x redirect_to admin_jam_track_rights_path, notice: "Could not order #{jam_track} for #{user.to_s}: #{x.errors.inspect}" else diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index ba112d2e1..2dabcecc1 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -11,7 +11,7 @@ FactoryGirl.define do state "NC" country "US" terms_of_service true - resue_card true + reuse_card true factory :admin do diff --git a/db/manifest b/db/manifest index 157316e1c..68e4fb17d 100755 --- a/db/manifest +++ b/db/manifest @@ -262,4 +262,4 @@ jam_track_importer.sql jam_track_pro_licensing_update.sql jam_track_redeemed.sql shopping_cart_anonymous.sql -user_reuse_card.sql \ No newline at end of file +user_reuse_card_and_reedem.sql \ No newline at end of file diff --git a/db/up/user_reuse_card.sql b/db/up/user_reuse_card.sql deleted file mode 100644 index 27ac34d5b..000000000 --- a/db/up/user_reuse_card.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE users ADD COLUMN reuse_card BOOLEAN DEFAULT TRUE NOT NULL; \ No newline at end of file diff --git a/db/up/user_reuse_card_and_reedem.sql b/db/up/user_reuse_card_and_reedem.sql new file mode 100644 index 000000000..2f2b811ea --- /dev/null +++ b/db/up/user_reuse_card_and_reedem.sql @@ -0,0 +1,3 @@ +ALTER TABLE users ADD COLUMN reuse_card BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE users ADD COLUMN has_redeemable_jamtrack BOOLEAN DEFAULT TRUE NOT NULL; +ALTER TABLE shopping_carts ADD COLUMN marked_for_redeem INTEGER DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index e8964f349..048a3d85b 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -21,5 +21,9 @@ module JamRuby def admin false end + + def has_redeemable_jamtrack + false + end end end diff --git a/ruby/lib/jam_ruby/models/mix.rb b/ruby/lib/jam_ruby/models/mix.rb index f441a441f..be833326c 100644 --- a/ruby/lib/jam_ruby/models/mix.rb +++ b/ruby/lib/jam_ruby/models/mix.rb @@ -141,12 +141,14 @@ module JamRuby recording.recorded_tracks.each do |recorded_track| manifest["files"] << { "filename" => recorded_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } - mix_params << { "level" => 1.0, "balance" => 0 } + mix_params << { "level" => 100, "balance" => 0 } + # change to 1.0 when ready to deploy new audiomixer end recording.recorded_backing_tracks.each do |recorded_backing_track| manifest["files"] << { "filename" => recorded_backing_track.sign_url(one_day), "codec" => "vorbis", "offset" => 0 } - mix_params << { "level" => 1.0, "balance" => 0 } + mix_params << { "level" => 100, "balance" => 0 } + # change to 1.0 when ready to deploy new audiomixer end recording.recorded_jam_track_tracks.each do |recorded_jam_track_track| diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index 401278d84..24f4db02c 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -8,19 +8,32 @@ module JamRuby validates :cart_id, presence: true validates :cart_type, presence: true validates :cart_class_name, presence: true + validates :marked_for_redeem, :inclusion => {:in => [true, false]} default_scope order('created_at DESC') def product_info product = self.cart_product - {name: product.name, price: product.price, product_id: cart_id} unless product.nil? + {name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, total_price: total_price(product), quantity: quantity, marked_for_redeem:marked_for_redeem} unless product.nil? end + # multiply (quantity - redeemable) by price + def total_price(product) + (quantity - marked_for_redeem) * product.price + end def cart_product self.cart_class_name.classify.constantize.find_by_id self.cart_id unless self.cart_class_name.blank? end - def self.create user, product, quantity = 1 + def redeem(mark_redeem) + self.marked_for_redeem = mark_redeem ? 1 : 0 + end + + def free? + marked_for_redeem == quantity + end + + def self.create user, product, quantity = 1, mark_redeem = false cart = ShoppingCart.new if user.is_a?(User) cart.user = user @@ -32,8 +45,27 @@ module JamRuby cart.cart_class_name = product.class.name cart.cart_id = product.id cart.quantity = quantity + cart.redeem(mark_redeem) cart.save cart end + + # if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked. + # if no shpping carts have been marked, then mark it redeemable + # should be wrapped in a TRANSACTION + def self.user_has_redeemable_jam_track?(any_user) + mark_redeem = false + if APP_CONFIG.one_free_jamtrack_per_user && any_user.has_redeemable_jamtrack + mark_redeem = true # start out assuming we can redeem... + shopping_carts.each do |shopping_cart| + # but if we find any shopping cart item already marked for redeem, then back out of mark_redeem=true + if shopping_cart.cart_type == product.class::PRODUCT_TYPE && shopping_cart.marked_for_redeem > 0 + mark_redeem = false + break + end + end + end + mark_redeem + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 447438190..8b8b3deed 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -172,6 +172,7 @@ module JamRuby validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false } validates :reuse_card, :inclusion => {:in => [true, false]} + validates :has_redeemable_jamtrack, :inclusion => {:in => [true, false]} validates :subscribe_email, :inclusion => {:in => [nil, true, false]} validates :musician, :inclusion => {:in => [true, false]} validates :show_whats_next, :inclusion => {:in => [nil, true, false]} diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index 96c4df57e..955f702fe 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -4,7 +4,7 @@ module JamRuby def initialize() end - def create_account(current_user, billing_info=nil) + def create_account(current_user, billing_info) options = account_hash(current_user, billing_info) account = nil begin @@ -37,7 +37,7 @@ module JamRuby end def get_account(current_user) - (current_user && current_user.recurly_code) ? Recurly::Account.find(current_user.recurly_code) : nil + current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil end def update_account(current_user, billing_info=nil) @@ -53,12 +53,12 @@ module JamRuby account end - def update_billing_info(current_user, billing_info=nil) + def update_billing_info(current_user, billing_info) account = get_account(current_user) if (account.present?) begin - account.billing_info=billing_info - account.billing_info.save + account.billing_info = billing_info + account.billing_info.save rescue Recurly::Error, NoMethodError => x raise RecurlyClientError, x.to_s end @@ -145,7 +145,7 @@ module JamRuby raise RecurlyClientError.new(plan.errors) if plan.errors.any? end - def place_order(current_user, jam_track) + def place_order(current_user, jam_track, shopping_cart) jam_track_right = nil account = get_account(current_user) if (account.present?) @@ -163,10 +163,17 @@ module JamRuby # this means we already have a subscription, so don't try to create a new one for the same plan (Recurly would fail this anyway) unless recurly_subscription_uuid - subscription = Recurly::Subscription.create(:account=>account, :plan_code=>jam_track.plan_code) + # if the shopping cart was specified, see if the item should be free + free = shopping_cart.nil? ? false : shopping_cart.free? + # and if it's free, squish the charge to 0. + unit_amount_in_cents = free ? 0 : nil + subscription = Recurly::Subscription.create(:account=>account, :plan_code=>jam_track.plan_code, unit_amount_in_cents: unit_amount_in_cents) raise RecurlyClientError.new(subscription.errors) if subscription.errors.any? + # delete from shopping cart the subscription + shopping_cart.destroy if shopping_cart + # Reload and make sure it went through: account = get_account(current_user) @@ -180,7 +187,7 @@ module JamRuby raise RecurlyClientError, "Plan code '#{paid_subscription.plan_code}' doesn't match jam track: '#{jam_track.plan_code}'" unless recurly_subscription_uuid - jam_track_right=JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) + jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) if jam_track_right.recurly_subscription_uuid != recurly_subscription_uuid jam_track_right.recurly_subscription_uuid = recurly_subscription_uuid jam_track_right.save @@ -198,7 +205,7 @@ module JamRuby jam_track_right end - def find_or_create_account(current_user, billing_info=nil) + def find_or_create_account(current_user, billing_info) account = get_account(current_user) if(account.nil?) diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index c81f6d38e..418399a8f 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -18,7 +18,7 @@ FactoryGirl.define do musician true terms_of_service true last_jam_audio_latency 5 - resue_card true + reuse_card true #u.association :musician_instrument, factory: :musician_instrument, user: u diff --git a/ruby/spec/jam_ruby/recurly_client_spec.rb b/ruby/spec/jam_ruby/recurly_client_spec.rb index 0245c91e9..078ad8a5c 100644 --- a/ruby/spec/jam_ruby/recurly_client_spec.rb +++ b/ruby/spec/jam_ruby/recurly_client_spec.rb @@ -89,7 +89,7 @@ describe RecurlyClient do it "can place order" do @client.find_or_create_account(@user, @billing_info) - expect{@client.place_order(@user, @jamtrack)}.not_to raise_error() + expect{@client.place_order(@user, @jamtrack, nil)}.not_to raise_error() subs = @client.get_account(@user).subscriptions subs.should_not be_nil subs.should have(1).items @@ -102,7 +102,7 @@ describe RecurlyClient do @client.find_or_create_account(@user, @billing_info) # Place order: - expect{@client.place_order(@user, @jamtrack)}.not_to raise_error() + expect{@client.place_order(@user, @jamtrack, nil)}.not_to raise_error() active_subs=@client.get_account(@user).subscriptions.find_all{|t|t.state=='active'} @jamtrack.reload @jamtrack.jam_track_rights.should have(1).items @@ -118,10 +118,10 @@ describe RecurlyClient do it "detects error on double order" do @client.find_or_create_account(@user, @billing_info) - jam_track_right = @client.place_order(@user, @jamtrack) + jam_track_right = @client.place_order(@user, @jamtrack, nil) jam_track_right.recurly_subscription_uuid.should_not be_nil - jam_track_right2 = @client.place_order(@user, @jamtrack) + jam_track_right2 = @client.place_order(@user, @jamtrack, nil) jam_track_right.should eq(jam_track_right2) jam_track_right.recurly_subscription_uuid.should eq(jam_track_right.recurly_subscription_uuid) end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index cab08fab2..94294b86c 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -166,6 +166,10 @@ def app_config 20 # 20 seconds end + def one_free_jamtrack_per_user + true + end + private diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js new file mode 100644 index 000000000..53bcc7330 --- /dev/null +++ b/web/app/assets/javascripts/checkout_order.js @@ -0,0 +1,368 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.CheckoutOrderScreen = function (app) { + + var EVENTS = context.JK.EVENTS; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var jamTrackUtils = context.JK.JamTrackUtils; + + var $screen = null; + var $navigation = null; + var $templateOrderContent = null; + var $templatePurchasedJamTrack = null; + var $orderPanel = null; + var $thanksPanel = null; + var $jamTrackInBrowser = null; + var $purchasedJamTrack = null; + var $purchasedJamTrackHeader = null; + var $purchasedJamTracks = null; + var $orderContent = null; + var userDetail = null; + var step = null; + var downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; + var $backBtn = null; + var $paymentPrompt = null; + var $emptyCartPrompt = null; + + + function beforeShow() { + beforeShowOrder(); + } + + + function afterShow(data) { + + } + + + function beforeHide() { + if(downloadJamTracks) { + context._.each(downloadJamTracks, function(downloadJamTrack) { + downloadJamTrack.destroy(); + downloadJamTrack.root.remove(); + }) + + downloadJamTracks = []; + } + purchasedJamTracks = null; + purchasedJamTrackIterator = 0; + } + + function beforeShowOrder() { + $paymentPrompt.addClass('hidden') + $emptyCartPrompt.addClass('hidden') + $orderPanel.removeClass("hidden") + $thanksPanel.addClass("hidden") + $orderContent.find(".place-order").addClass('disabled').off('click', placeOrder) + step = 3; + renderNavigation(); + populateOrderPage(); + } + + + function populateOrderPage() { + clearOrderPage(); + + rest.getShoppingCarts() + .done(function(carts) { + rest.getBillingInfo() + .done(function(billingInfo) { + renderOrderPage(carts, billingInfo) + }) + .fail(function(jqXHR) { + if(jqXHR.status == 404) { + // no account for this user + app.notify({ title: "No account information", + text: "Please restart the checkout process." }, + null, + true); + } + }) + }) + .fail(app.ajaxError); + } + + + function renderOrderPage(carts, recurlyAccountInfo) { + logger.debug("rendering order page") + var data = {} + + var sub_total = 0.0 + var taxes = 0.0 + $.each(carts, function(index, cart) { + sub_total += parseFloat(cart.product_info.total_price) + }); + //data.grand_total = (sub_total + taxes).toFixed(2) + data.sub_total = sub_total.toFixed(2) + //data.taxes = taxes.toFixed(2) + data.carts = carts + data.billing_info = recurlyAccountInfo.billing_info + data.shipping_info = recurlyAccountInfo.address + + data.shipping_as_billing = true; //jamTrackUtils.compareAddress(data.billing_info, data.shipping_info); + + var orderContentHtml = $( + context._.template( + $templateOrderContent.html(), + data, + {variable: 'data'} + ) + ) + + $orderContent.append(orderContentHtml) + $orderPanel.find(".change-payment-info").on('click', moveToPaymentInfo) + + var $placeOrder = $screen.find(".place-order") + if(carts.length == 0) { + $paymentPrompt.addClass('hidden') + $emptyCartPrompt.removeClass('hidden') + $placeOrder.addClass('disabled') + } + else { + logger.debug("cart has " + carts.length + " items in it") + $paymentPrompt.removeClass('hidden') + $emptyCartPrompt.addClass('hidden') + $placeOrder.removeClass('disabled').on('click', placeOrder) + + var planPricing = {} + + context._.each(carts, function(cart) { + var priceElement = $screen.find('.order-right-page .plan[data-plan-code="' + cart.product_info.plan_code +'"]') + + if(priceElement.length == 0) { + logger.error("unable to find price element for " + cart.product_info.plan_code, cart); + app.notify({title: "Error Encountered", text: "Unable to find plan info for " + cart.product_info.plan_code}) + return false; + } + + logger.debug("creating recurly pricing element for plan: " + cart.product_info.plan_code) + var pricing = context.recurly.Pricing(); + pricing.plan_code = cart.product_info.plan_code; + pricing.resolved = false; + pricing.effective_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem + planPricing[pricing.plan_code] = pricing; + + // this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it. + pricing.on('change', function(price) { + + var resolvedPrice = planPricing[this.plan_code]; + if(!resolvedPrice) { + logger.error("unable to find price info in storage") + app.notify({title: "Error Encountered", text: "Unable to find plan info in storage"}) + return; + } + else { + logger.debug("pricing resolved for plan: " + this.plan_code) + } + resolvedPrice.resolved = true; + + var allResolved = true; + var totalTax = 0; + var totalPrice = 0; + + // let's see if all plans have been resolved via API; and add up total price and taxes for display + $.each(planPricing, function(plan_code, priceObject) { + logger.debug("resolved recurly priceObject", priceObject) + + if(!priceObject.resolved) { + allResolved = false; + return false; + } + else { + var unitTax = Number(priceObject.price.now.tax) * priceObject.effective_quantity; + totalTax += unitTax; + + var totalUnitPrice = Number(priceObject.price.now.total) * priceObject.effective_quantity; + totalPrice += totalUnitPrice; + } + }) + + if(allResolved) { + $screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2)) + $screen.find('.order-right-page .order-items-value.order-total').text('$' + totalPrice.toFixed(2)) + } + else + { + logger.debug("still waiting on more plans to resolve") + } + }) + pricing.attach(priceElement.eq(0)) + }) + } + } + + function moveToPaymentInfo() { + context.location = '/client#/checkoutPayment'; + return false; + } + + function placeOrder(e) { + e.preventDefault(); + $screen.find(".place-order").off('click').addClass('disabled') + rest.placeOrder() + .done(moveToThanks) + .fail(orderErrorHandling); + } + + + function orderErrorHandling(xhr, ajaxOptions, thrownError) { + if (xhr && xhr.responseJSON) { + var message = "Error submitting payment: " + $.each(xhr.responseJSON.errors, function (key, error) { + message += key + ": " + error + }) + $("#order_error").text(message).removeClass("hidden") + } + else { + $("#order_error").text(xhr.responseText).removeClass("hidden") + } + $orderContent.find(".place-order").on('click', placeOrder) + } + + function moveToThanks(purchaseResponse) { + $("#order_error").addClass("hidden") + $orderPanel.addClass("hidden") + $thanksPanel.removeClass("hidden") + jamTrackUtils.checkShoppingCart() + //beforeShowOrder() + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + } + } + } + + function startDownloadJamTracks(jamTracks) { + // there can be multiple purchased JamTracks, so we cycle through them + + purchasedJamTracks = jamTracks; + + // populate list of jamtracks purchased, that we will iterate through graphically + context._.each(jamTracks, function(jamTrack) { + var downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'small'); + var $purchasedJamTrack = $(context._.template( + $templatePurchasedJamTrack.html(), + jamTrack, + {variable: 'data'} + )); + + $purchasedJamTracks.append($purchasedJamTrack) + + // show it on the page + $purchasedJamTrack.append(downloadJamTrack.root) + + downloadJamTracks.push(downloadJamTrack) + }) + + iteratePurchasedJamTracks(); + } + + function iteratePurchasedJamTracks() { + if(purchasedJamTrackIterator < purchasedJamTracks.length ) { + var downloadJamTrack = downloadJamTracks[purchasedJamTrackIterator++]; + + // make sure the 'purchasing JamTrack' section can be seen + $purchasedJamTrack.removeClass('hidden'); + + // the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { + + if(data.state == downloadJamTrack.states.synchronized) { + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " synchronized;") + //downloadJamTrack.root.remove(); + downloadJamTrack.destroy(); + + // go to the next JamTrack + iteratePurchasedJamTracks() + } + }) + + logger.debug("jamtrack " + downloadJamTrack.jamTrack.name + " downloader initializing") + + // kick off the download JamTrack process + downloadJamTrack.init() + + // XXX style-test code + // downloadJamTrack.transitionError("package-error", "The server failed to create your package.") + + } + else { + logger.debug("done iterating over purchased JamTracks") + $purchasedJamTrackHeader.text('All purchased JamTracks have been downloaded successfully! You can now play them in a session.') + } + } + + function clearOrderPage() { + $orderContent.empty(); + } + + function renderNavigation() { + $navigation.html(""); + var navigationHtml = $( + context._.template( + $('#template-checkout-navigation').html(), + {current: step}, + {variable: 'data'} + ) + ); + + $navigation.append(navigationHtml); + } + + function events() { + $backBtn.on('click', function(e) { + e.preventDefault(); + + context.location = '/client#/checkoutPayment' + }) + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow, + 'beforeHide': beforeHide + }; + app.bindScreen('checkoutOrder', screenBindings); + + $screen = $("#checkoutOrderScreen"); + $navigation = $screen.find(".checkout-navigation-bar"); + $templateOrderContent = $("#template-order-content"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $orderPanel = $screen.find(".order-panel"); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".thanks-detail.jam-tracks-in-browser"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $backBtn = $screen.find('.back'); + $paymentPrompt = $screen.find('.payment-prompt'); + $emptyCartPrompt = $screen.find('.empty-cart-prompt'); + $orderContent = $orderPanel.find(".order-content"); + + if ($screen.length == 0) throw "$screen must be specified"; + if ($navigation.length == 0) throw "$navigation must be specified"; + + events(); + } + + this.initialize = initialize; + + return this; + } +}) +(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js index aefe06be6..c492ddf3e 100644 --- a/web/app/assets/javascripts/checkout_payment.js +++ b/web/app/assets/javascripts/checkout_payment.js @@ -6,6 +6,7 @@ var EVENTS = context.JK.EVENTS; var logger = context.JK.logger; + var jamTrackUtils = context.JK.JamTrackUtils; var $screen = null; var $navigation = null; @@ -21,8 +22,17 @@ var billing_info = null; var shipping_info = null; var shipping_as_billing = null; + var $reuseExistingCard = null; + var $reuseExistingCardChk = null; + var $existingCardEndsWith = null; + var $newCardInfo = null; + var selectCountry = null; + var selectCountryLoaded = false; + var $freeJamTrackPrompt = null; + var $noFreeJamTrackPrompt = null; + + function afterShow() { - function beforeShow() { beforeShowPaymentInfo(); } @@ -30,39 +40,76 @@ step = 2; renderNavigation(); renderAccountInfo(); + } function renderAccountInfo() { - var user = rest.getUserDetail() + $reuseExistingCard.addClass('hidden'); + $newCardInfo.removeClass('hidden'); + $freeJamTrackPrompt.addClass('hidden'); + $noFreeJamTrackPrompt.addClass('hidden'); + + var selectCountryReady = selectCountry.ready(); + if(!selectCountryReady) { + // one time init of country dropdown + selectCountryReady = selectCountry.load('US', null, null); + } + + selectCountryReady.done(function() { + var user = rest.getUserDetail() if(user) { user.done(populateAccountInfo).error(app.ajaxError); } + }) } function populateAccountInfo(user) { userDetail = user; + $reuseExistingCardChk.iCheck(userDetail.reuse_card ? 'check' : 'uncheck').attr('checked', userDetail.reuse_card) + + // show appropriate prompt text based on whether user has a free jamtrack + if(user.free_jamtrack) { + $freeJamTrackPrompt.removeClass('hidden') + } + else { + $noFreeJamTrackPrompt.removeClass('hidden') + } + if (userDetail.has_recurly_account) { + rest.getBillingInfo() .done(function(response) { - $billingInfo.find("#billing-first-name").val(response.first_name); - $billingInfo.find("#billing-last-name").val(response.last_name); - $billingInfo.find("#billing-address1").val(response.address1); - $billingInfo.find("#billing-address2").val(response.address2); - $billingInfo.find("#billing-city").val(response.city); - $billingInfo.find("#billing-state").val(response.state); - $billingInfo.find("#billing-zip").val(response.zip); - $billingInfo.find("#billing-country").val(response.country); - $shippingAddress.find("#shipping-first-name").val(response.first_name); - $shippingAddress.find("#shipping-last-name").val(response.last_name); - $shippingAddress.find("#shipping-address1").val(response.address1); - $shippingAddress.find("#shipping-address2").val(response.address2); - $shippingAddress.find("#shipping-city").val(response.city); - $shippingAddress.find("#shipping-state").val(response.state); - $shippingAddress.find("#shipping-zip").val(response.zip); - $shippingAddress.find("#shipping-country").val(response.country); + if(userDetail.reuse_card) { + $reuseExistingCard.removeClass('hidden'); + toggleReuseExistingCard.call($reuseExistingCardChk) + $existingCardEndsWith.text(response.billing_info.last_four); + } + + var isSameAsShipping = true // jamTrackUtils.compareAddress(response.billing_info, response.address); + + $shippingAsBilling.iCheck(isSameAsShipping ? 'check' : 'uncheck').attr('checked', isSameAsShipping) + + $billingInfo.find("#billing-first-name").val(response.billing_info.first_name); + $billingInfo.find("#billing-last-name").val(response.billing_info.last_name); + $billingInfo.find("#billing-address1").val(response.billing_info.address1); + $billingInfo.find("#billing-address2").val(response.billing_info.address2); + $billingInfo.find("#billing-city").val(response.billing_info.city); + $billingInfo.find("#billing-state").val(response.billing_info.state); + $billingInfo.find("#billing-zip").val(response.billing_info.zip); + $billingInfo.find("#billing-country").val(response.billing_info.country); + + //$shippingAddress.find("#shipping-first-name").val(response.billing_info.first_name); + //$shippingAddress.find("#shipping-last-name").val(response.billing_info.last_name); + //$shippingAddress.find("#shipping-address1").val(response.address.address1); + //$shippingAddress.find("#shipping-address2").val(response.address.address2); + //$shippingAddress.find("#shipping-city").val(response.address.city); + //$shippingAddress.find("#shipping-state").val(response.address.state); + //$shippingAddress.find("#shipping-zip").val(response.address.zip); + //$shippingAddress.find("#shipping-country").val(response.address.country); + }) .error(app.ajaxError); } @@ -81,7 +128,7 @@ } } - function afterShow(data) { + function beforeShow(data) { // XXX : style-test code // moveToThanks({jam_tracks: [{id: 14, jam_track_right_id: 11, name: 'Back in Black'}, {id: 15, jam_track_right_id: 11, name: 'In Bloom'}, {id: 16, jam_track_right_id: 11, name: 'Love Bird Supreme'}]}); } @@ -96,8 +143,14 @@ // TODO: Refactor: this function is long and fraught with many return points. function next(e) { + $paymentInfoPanel.find('.error-text').remove(); + $paymentInfoPanel.find('.error').removeClass('error'); e.preventDefault(); - $("#order_error").addClass("hidden") + $("#payment_error").addClass("hidden") + + var reuse_card_this_time = $reuseExistingCardChk.is(':checked'); + var reuse_card_next_time = $paymentMethod.find('#save-card').is(':checked'); + // validation var billing_first_name = $billingInfo.find("#billing-first-name").val(); @@ -114,6 +167,7 @@ $billingInfo.find('#divBillingFirstName').addClass("error"); $billingInfo.find('#billing-first-name').after(""); + logger.info("no billing first name"); return false; } else { @@ -125,6 +179,7 @@ $billingInfo.find('#divBillingLastName').addClass("error"); $billingInfo.find('#billing-last-name').after(""); + logger.info("no billing last name"); return false; } else { @@ -136,6 +191,7 @@ $billingInfo.find('#divBillingAddress1').addClass("error"); $billingInfo.find('#billing-address1').after(""); + logger.info("no billing address line 1"); return false; } else { @@ -147,6 +203,7 @@ $billingInfo.find('#divBillingZip').addClass("error"); $billingInfo.find('#billing-zip').after(""); + logger.info("no billing address line 2"); return false; } else { @@ -158,6 +215,7 @@ $billingInfo.find('#divBillingState').addClass("error"); $billingInfo.find('#billing-zip').after(""); + logger.info("no billing zip"); return false; } else { @@ -169,6 +227,7 @@ $billingInfo.find('#divBillingCity').addClass("error"); $billingInfo.find('#billing-city').after(""); + logger.info("no billing city"); return false; } else { @@ -180,12 +239,14 @@ $billingInfo.find('#divBillingCountry').addClass("error"); $billingInfo.find('#billing-country').after(""); + logger.info("no billing country"); return false; } else { $billingInfo.find('#divBillingCountry').removeClass("error"); } + /** shipping_as_billing = $shippingAsBilling.is(":checked"); var shipping_first_name, shipping_last_name, shipping_address1, shipping_address2; var shipping_city, shipping_state, shipping_zip, shipping_country; @@ -205,6 +266,7 @@ $shippingAddress.find('#divShippingFirstName').addClass("error"); $shippingAddress.find('#shipping-first-name').after(""); + logger.info("no address first name"); return false; } else { @@ -216,6 +278,7 @@ $shippingAddress.find('#divShippingLastName').addClass("error"); $shippingAddress.find('#shipping-last-name').after(""); + logger.info("no last name"); return false; } else { @@ -227,6 +290,7 @@ $shippingAddress.find('#divShippingAddress1').addClass("error"); $shippingAddress.find('#shipping-address1').after(""); + logger.info("no shipping address 1"); return false; } else { @@ -238,6 +302,7 @@ $shippingAddress.find('#divShippingZip').addClass("error"); $shippingAddress.find('#shipping-zip').after(""); + logger.info("no shipping address 2"); return false; } else { @@ -249,6 +314,7 @@ $shippingAddress.find('#divShippingState').addClass("error"); $shippingAddress.find('#shipping-zip').after(""); + logger.info("no shipping state"); return false; } else { @@ -260,6 +326,7 @@ $shippingAddress.find('#divShippingCity').addClass("error"); $shippingAddress.find('#shipping-city').after(""); + logger.info("no shipping city"); return false; } else { @@ -271,12 +338,14 @@ $shippingAddress.find('#divShippingCountry').addClass("error"); $shippingAddress.find('#shipping-country').after(""); + logger.info("no shipping country"); return false; } else { $shippingAddress.find('#divShippingCountry').removeClass("error"); } } + */ var card_name = $paymentMethod.find("#card-name").val(); var card_number = $paymentMethod.find("#card-number").val(); @@ -284,6 +353,7 @@ var card_month = $paymentMethod.find("#card_expire-date_2i").val(); var card_verify = $paymentMethod.find("#card-verify").val(); + /** if (!card_name) { $paymentMethod.find('#divCardName .error-text').remove(); $paymentMethod.find('#divCardName').addClass("error"); @@ -291,46 +361,54 @@ return false; } else { $paymentMethod.find('#divCardName').removeClass("error"); + }*/ + + // don't valid card form fields when reuse card selected + if(!reuse_card_this_time) { + if (!card_number) { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error"); + $paymentMethod.find('#card-number').after(""); + logger.info("no card number"); + return false; + } else if (!$.payment.validateCardNumber(card_number)) { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error"); + $paymentMethod.find('#card-number').after(""); + logger.info("invalid card number"); + return false; + } else { + $paymentMethod.find('#divCardNumber').removeClass("error"); + } + + if (!$.payment.validateCardExpiry(card_month, card_year)) { + $paymentMethod.find('#divCardExpiry .error-text').remove(); + $paymentMethod.find('#divCardExpiry').addClass("error"); + $paymentMethod.find('#card-expiry').after(""); + logger.info("invalid card expiry"); + return false; + } else { + $paymentMethod.find('#divCardExpiry').removeClass("error"); + } + + if (!card_verify) { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error"); + $paymentMethod.find('#card-verify').after(""); + + logger.info("no card verify"); + return false; + } else if (!$.payment.validateCardCVC(card_verify)) { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error"); + $paymentMethod.find('#card-verify').after(""); + + logger.info("bad card CVC"); + return false; + } else { + $paymentMethod.find('#divCardVerify').removeClass("error"); + } } - - if (!card_number) { - $paymentMethod.find('#divCardNumber .error-text').remove(); - $paymentMethod.find('#divCardNumber').addClass("error"); - $paymentMethod.find('#card-number').after(""); - return false; - } else if (!$.payment.validateCardNumber(card_number)) { - $paymentMethod.find('#divCardNumber .error-text').remove(); - $paymentMethod.find('#divCardNumber').addClass("error"); - $paymentMethod.find('#card-number').after(""); - return false; - } else { - $paymentMethod.find('#divCardNumber').removeClass("error"); - } - - if (!$.payment.validateCardExpiry(card_month, card_year)) { - $paymentMethod.find('#divCardExpiry .error-text').remove(); - $paymentMethod.find('#divCardExpiry').addClass("error"); - $paymentMethod.find('#card-expiry').after(""); - } else { - $paymentMethod.find('#divCardExpiry').removeClass("error"); - } - - if (!card_verify) { - $paymentMethod.find('#divCardVerify .error-text').remove(); - $paymentMethod.find('#divCardVerify').addClass("error"); - $paymentMethod.find('#card-verify').after(""); - - return false; - } else if(!$.payment.validateCardCVC(card_verify)) { - $paymentMethod.find('#divCardVerify .error-text').remove(); - $paymentMethod.find('#divCardVerify').addClass("error"); - $paymentMethod.find('#card-verify').after(""); - - return false; - } else { - $paymentMethod.find('#divCardVerify').removeClass("error"); - } - billing_info = {}; shipping_info = {}; billing_info.first_name = billing_first_name; @@ -346,6 +424,7 @@ billing_info.year = card_year; billing_info.verification_value = card_verify; + /** if (shipping_as_billing) { shipping_info = $.extend({},billing_info); delete shipping_info.number; @@ -361,69 +440,83 @@ shipping_info.state = shipping_state; shipping_info.country = shipping_country; shipping_info.zip = shipping_zip; - } - - $paymentInfoPanel.find("#payment-info-next").addClass("disabled"); - $paymentInfoPanel.find("#payment-info-next").off("click"); - - var reuse_card = $paymentMethod.find('#save-card').is(':checked'); - + }*/ var email = null; var password = null; + var terms = false; var isLoggedIn = context.JK.currentUserId; if(!isLoggedIn) { email = $accountSignup.find('input[name="email"]').val() password = $accountSignup.find('input[name="password"]').val() + terms = $accountSignup.find('input[name="terms-of-service"]').is(':checked'); } - rest.createRecurlyAccount({billing_info: billing_info, terms_of_service: true, email: email, password: password, reuse_card: reuse_card}) + $screen.find("#payment-info-next").addClass("disabled"); + $screen.find("#payment-info-next").off("click"); + rest.createRecurlyAccount({billing_info: billing_info, terms_of_service: terms, email: email, password: password, reuse_card_this_time: reuse_card_this_time, reuse_card_next_time: reuse_card_next_time}) .done(function() { - $paymentInfoPanel.find("#payment-info-next").removeClass("disabled"); - $paymentInfoPanel.find("#payment-info-next").on("click", next); + $screen.find("#payment-info-next").on("click", next); if(isLoggedIn) { - context.location = '/client#checkout_order' + context.location = '/client#/checkoutOrder' } else { // this means the account was created; we need to reload the page for this to take effect - context.location = '/client#checkout_order' + context.JK.currentUserId = 'something' // this is to trick layout.js from getting involved and redirecting to home screen + context.location = '/client#/checkoutOrder' context.location.reload() } }) - .fail(errorHandling); + .fail(errorHandling) + .always(function(){ + $screen.find("#payment-info-next").removeClass("disabled"); + }) } function errorHandling(xhr, ajaxOptions, thrownError) { - $.each(xhr.responseJSON.errors, function(key, error) { - if (key == 'number') { - $paymentMethod.find('#divCardNumber .error-text').remove(); - $paymentMethod.find('#divCardNumber').addClass("error"); - $paymentMethod.find('#card-number').after(""); - } - else if (key == 'verification_value') { - $paymentMethod.find('#divCardVerify .error-text').remove(); - $paymentMethod.find('#divCardVerify').addClass("error"); - $paymentMethod.find('#card-verify').after(""); - } - else if(key == 'email') { - var $email = $accountSignup.find('input[name="email"]') - var $field = $email.closest('.field') - $field.find('.error-text').remove() - $field.addClass("error"); - $field.append(""); - } - else if(key == 'password') { - var $email = $accountSignup.find('input[name="password"]') - var $field = $email.closest('.field') - $field.find('.error-text').remove() - $field.addClass("error"); - $field.append(""); - } - }); + logger.debug("error handling", xhr.responseJSON) + if(xhr.responseJSON && xhr.responseJSON.errors) { + $.each(xhr.responseJSON.errors, function(key, error) { + if (key == 'number') { + $paymentMethod.find('#divCardNumber .error-text').remove(); + $paymentMethod.find('#divCardNumber').addClass("error"); + $paymentMethod.find('#card-number').after(""); + } + else if (key == 'verification_value') { + $paymentMethod.find('#divCardVerify .error-text').remove(); + $paymentMethod.find('#divCardVerify').addClass("error"); + $paymentMethod.find('#card-verify').after(""); + } + else if(key == 'email') { + var $email = $accountSignup.find('input[name="email"]') + var $field = $email.closest('.field') + $field.find('.error-text').remove() + $field.addClass("error"); + $field.append(""); + } + else if(key == 'password') { + var $password = $accountSignup.find('input[name="password"]') + var $field = $password.closest('.field') + $field.find('.error-text').remove() + $field.addClass("error"); + $field.append(""); + } + else if(key == 'terms_of_service') { + var $terms = $accountSignup.find('input[name="terms-of-service"]') + var $field = $terms.closest('.field') + $field.find('.error-text').remove() + $field.addClass("error"); + $field.append(""); + } + }); + } + else { + $("#payment_error").text(xhr.responseText) + } - $paymentInfoPanel.find("#payment-info-next").addClass("disabled"); - $paymentInfoPanel.find("#payment-info-next").on('click', next); + + $screen.find("#payment-info-next").on('click', next); } function beforeShowOrder() { @@ -440,10 +533,6 @@ .fail(app.ajaxError); } - function moveToOrder() { - window.location = '/client#/checkout_order'; - } - function toggleShippingAsBilling(e) { e.preventDefault(); @@ -457,9 +546,36 @@ } } + function toggleReuseExistingCard(e) { + if(e) { + e.preventDefault(); + } + + logger.debug("toggle reuse existing card") + + var reuse_existing = $(this).is(':checked'); + + $('#billing-first-name').prop('disabled', reuse_existing); + $('#billing-last-name').prop('disabled', reuse_existing); + $('#billing-address1').prop('disabled', reuse_existing); + $('#billing-address2').prop('disabled', reuse_existing); + $('#billing-city').prop('disabled', reuse_existing); + $('#billing-state').prop('disabled', reuse_existing); + $('#billing-zip').prop('disabled', reuse_existing); + $('#billing-country').prop('disabled', reuse_existing); + + $('#card-name').prop('disabled', reuse_existing); + $('#card-number').prop('disabled', reuse_existing); + $('#card_expire-date_1i').prop('disabled', reuse_existing); + $('#card_expire-date_2i').prop('disabled', reuse_existing); + $('#card-verify').prop('disabled', reuse_existing); + + } + function events() { $screen.find("#payment-info-next").on('click', next); $shippingAsBilling.on('ifChanged', toggleShippingAsBilling); + $reuseExistingCardChk.on('ifChanged', toggleReuseExistingCard); } function reset() { @@ -489,6 +605,8 @@ // Use jquery.payment to limit characters and length: $paymentMethod.find("#card-number").payment('formatCardNumber'); $paymentMethod.find("#card-verify").payment('formatCardCVC'); + + selectCountry = new context.JK.SelectLocation($('#billing-country'), null, null, app, false) } function initialize() { @@ -497,7 +615,7 @@ 'afterShow': afterShow, 'beforeHide' : beforeHide }; - app.bindScreen('checkout_payment', screenBindings); + app.bindScreen('checkoutPayment', screenBindings); $screen = $("#checkoutPaymentScreen"); $paymentInfoPanel = $screen.find("#checkout-payment-info"); @@ -508,6 +626,13 @@ $accountSignup = $paymentInfoPanel.find('.jamkazam-account-signup') $shippingAddress = $paymentInfoPanel.find(".shipping-address-detail"); $shippingAsBilling = $paymentInfoPanel.find("#shipping-as-billing"); + $reuseExistingCard = $paymentInfoPanel.find('.reuse-existing-card') + $reuseExistingCardChk = $paymentInfoPanel.find('#reuse-existing-card') + $existingCardEndsWith = $paymentInfoPanel.find('.existing-card-ends-with') + $newCardInfo = $paymentInfoPanel.find('.new-card-info') + $freeJamTrackPrompt = $screen.find('.payment-prompt.free-jamtrack') + $noFreeJamTrackPrompt = $screen.find('.payment-prompt.no-free-jamtrack') + if($screen.length == 0) throw "$screen must be specified"; if($navigation.length == 0) throw "$navigation must be specified"; diff --git a/web/app/assets/javascripts/checkout_signin.js b/web/app/assets/javascripts/checkout_signin.js index 26fb3a1ed..22ee88060 100644 --- a/web/app/assets/javascripts/checkout_signin.js +++ b/web/app/assets/javascripts/checkout_signin.js @@ -51,7 +51,7 @@ } function moveNext() { - window.location = '/client#/checkout_payment'; + window.location = '/client#/checkoutPayment'; return false; } @@ -69,7 +69,7 @@ rest.login({email: email, password: password, remember_me: true}) .done(function() { - window.location = '/client#/checkout_payment' + window.location = '/client#/checkoutPayment' window.location.reload(); }) .fail(function(jqXHR) { @@ -106,7 +106,7 @@ 'beforeShow': beforeShow, 'afterShow': afterShow }; - app.bindScreen('checkout_signin', screenBindings); + app.bindScreen('checkoutSignin', screenBindings); $screen = $("#checkoutSignInScreen"); $navigation = $screen.find(".checkout-navigation-bar"); diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index b8c0f62d5..01b7cf326 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -215,13 +215,7 @@ } function initShoppingCart(app) { - - var user = app.user() - if(user) { - user.done(function(userProfile) { - context.JK.JamTrackUtils.checkShoppingCart(); - }) - } + context.JK.JamTrackUtils.checkShoppingCart(); } })(window, jQuery); diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index a5ca73b2f..0eda8f103 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -473,7 +473,7 @@ processData: false, contentType: 'application/json', data: JSON.stringify(options) - }); + }) } function getUserDetail(options) { @@ -1547,10 +1547,10 @@ }); } - function placeOrder(options) { + function placeOrder() { return $.ajax({ type: "POST", - url: '/api/recurly/place_order?' + $.param(options), + url: '/api/recurly/place_order', dataType: "json", contentType: 'application/json' }); diff --git a/web/app/assets/javascripts/jam_track_utils.js.coffee b/web/app/assets/javascripts/jam_track_utils.js.coffee index 8ba73f592..d3da3180a 100644 --- a/web/app/assets/javascripts/jam_track_utils.js.coffee +++ b/web/app/assets/javascripts/jam_track_utils.js.coffee @@ -22,7 +22,12 @@ class JamTrackUtils else cartLink.addClass("hidden") - + compareAddress: (billing, shipping) => + billing.address1 == shipping.address1 && + billing.address2 == shipping.address2 && + billing.zip == shipping.zip && + billing.city == shipping.city && + billing.country == shipping.country; # global instance diff --git a/web/app/assets/javascripts/order.js b/web/app/assets/javascripts/order.js index 8fb111bab..b4f545723 100644 --- a/web/app/assets/javascripts/order.js +++ b/web/app/assets/javascripts/order.js @@ -115,19 +115,6 @@ purchasedJamTrackIterator = 0; } - function beforeHide() { - if(downloadJamTracks) { - context._.each(downloadJamTracks, function(downloadJamTrack) { - downloadJamTrack.destroy(); - downloadJamTrack.root.remove(); - }) - - downloadJamTracks = []; - } - purchasedJamTracks = null; - purchasedJamTrackIterator = 0; - } - // TODO: Refactor: this function is long and fraught with many return points. function next(e) { e.preventDefault(); diff --git a/web/app/assets/javascripts/selectLocation.js b/web/app/assets/javascripts/selectLocation.js index 4d5b36711..93cde1b12 100644 --- a/web/app/assets/javascripts/selectLocation.js +++ b/web/app/assets/javascripts/selectLocation.js @@ -6,7 +6,7 @@ context.JK.SelectLocation = Class.extend({ - init: function ($countries, $regions, $cities, app) { + init: function ($countries, $regions, $cities, app, useEasyDropdown) { this.api = context.JK.Rest(); this.logger = context.JK.logger; this.loadingCitiesData = false; @@ -14,9 +14,12 @@ this.loadingCountriesData = false; this.nilOptionStr = ''; this.nilOptionText = 'n/a'; + this.countriesLoaded = false; this.$countries = $countries; this.$regions = $regions; this.$cities = $cities; + this.$deferred = null; + this.useEasyDropdown = useEasyDropdown === undefined ? true : useEasyDropdown; this.app = app; $countries.on('change', function (evt) { @@ -24,11 +27,24 @@ this.handleCountryChanged(); return false; }.bind(this)); - $regions.on('change', function (evt) { - evt.stopPropagation(); - this.handleRegionChanged(); - return false; - }.bind(this)); + if($regions) { + $regions.on('change', function (evt) { + evt.stopPropagation(); + this.handleRegionChanged(); + return false; + }.bind(this)); + } + }, + selectCountry: function (country) { + if(this.useEasyDropdown) { + this.$countries.easyDropDown('select', country, true) + } + else { + this.$countries.val(country) + } + }, + ready: function() { + return this.$deferred; }, load: function (country, region, city) { @@ -42,13 +58,9 @@ country = 'US'; } - this.loadingCountriesData = true; - this.loadingRegionsData = true; - this.loadingCitiesData = true; - // make the 3 slower requests, which only matter if the user wants to affect their ISP or location - - this.api.getCountries() + this.loadingCountriesData = true; + this.$deferred = this.api.getCountries() .done(function (countriesx) { this.populateCountriesx(countriesx["countriesx"], country); }.bind(this)) @@ -57,7 +69,9 @@ this.loadingCountriesData = false; }.bind(this)) - if (country) { + + if (country && this.$regions) { + this.loadingRegionsData = true; this.api.getRegions({ country: country }) .done(function (regions) { this.populateRegions(regions["regions"], region); @@ -67,7 +81,8 @@ this.loadingRegionsData = false; }.bind(this)) - if (region) { + if (region && this.$cities) { + this.loadingCitiesData = true; this.api.getCities({ country: country, region: region }) .done(function (cities) { this.populateCities(cities["cities"], this.city) @@ -78,9 +93,15 @@ }.bind(this)) } } + return this.$deferred; }, handleCountryChanged: function () { var selectedCountry = this.$countries.val() + + if(!this.$regions) { + return; + } + var selectedRegion = this.$regions.val() var cityElement = this.$cities @@ -144,7 +165,9 @@ else { cityElement.children().remove(); cityElement.append($(this.nilOptionStr).text(this.nilOptionText)); - context.JK.dropdown(cityElement); + if(this.useEasyDropdown) { + context.JK.dropdown(cityElement); + } } }, @@ -159,7 +182,7 @@ if (!countryx.countrycode) return; var option = $(this.nilOptionStr); - option.text(countryx.countryname); + option.text(countryx.countryname ? countryx.countryname : countryx.countrycode); option.attr("value", countryx.countrycode); if (countryx.countrycode == this.country) { @@ -170,6 +193,8 @@ }, populateCountriesx: function (countriesx) { + this.countriesLoaded = true; + // countriesx has the format [{countrycode: "US", countryname: "United States"}, ...] this.foundCountry = false; @@ -194,8 +219,9 @@ this.$countries.val(this.country); this.$countries.attr("disabled", null).easyDropDown('enable'); - - context.JK.dropdown(this.$countries); + if(this.useEasyDropdown) { + context.JK.dropdown(this.$countries); + } }, writeRegion: function (index, region) { @@ -220,7 +246,9 @@ this.$regions.val(userRegion) this.$regions.attr("disabled", null).easyDropDown('enable'); - context.JK.dropdown(this.$regions); + if(this.useEasyDropdown) { + context.JK.dropdown(this.$regions); + } }, writeCity: function (index, city) { @@ -245,7 +273,9 @@ this.$cities.val(userCity) this.$cities.attr("disabled", null).easyDropDown('enable'); - context.JK.dropdown(this.$cities); + if(this.useEasyDropdown) { + context.JK.dropdown(this.$cities); + } }, regionListFailure: function (jqXHR, textStatus, errorThrown) { diff --git a/web/app/assets/javascripts/shopping_cart.js b/web/app/assets/javascripts/shopping_cart.js index d8856d520..eb782bd56 100644 --- a/web/app/assets/javascripts/shopping_cart.js +++ b/web/app/assets/javascripts/shopping_cart.js @@ -30,15 +30,15 @@ e.preventDefault(); if (!context.JK.currentUserId) { - window.location = '/client#/checkout_signin'; + window.location = '/client#/checkoutSignin'; } else { app.user().done(function(user) { if(user.reuse_card) { - window.location = '/client#/checkout_order'; + window.location = '/client#/checkoutOrder'; } else { - window.location = '/client#/checkout_payment'; + window.location = '/client#/checkoutPayment'; } }) diff --git a/web/app/assets/stylesheets/client/checkout.css.scss b/web/app/assets/stylesheets/client/checkout.css.scss index 5e3128cf1..7d5190ad8 100644 --- a/web/app/assets/stylesheets/client/checkout.css.scss +++ b/web/app/assets/stylesheets/client/checkout.css.scss @@ -64,88 +64,3 @@ } } } - -.order-panel { - padding: 30px; - - .order-header { - h2 { - font-size: 16px; - } - } - - .order-content { - margin-top: 20px; - } - - .order-left-page { - float: left; - width: 60%; - - .payment-info-page { - padding: 5px; - - .info-caption-link { - .caption-text { - float: left; - } - .caption-link { - float: left; - margin-left: 5px; - } - } - - .address-info { - width: 50%; - float: left; - } - - .payment-method-info { - width: 50%; - float: left; - } - } - .order-items-page { - padding: 5px; - - .cart-item-caption { - width: 50%; - text-align: left; - float: left; - } - - .cart-item-caption#header { - font-weight: bold; - } - - .cart-item-price { - width: 25%; - text-align: right; - float: left; - } - - .cart-item-quantity { - width: 25%; - text-align: right; - float: left; - } - - .cart-items { - margin-top: 10px; - } - - .cart-item { - margin-top: 10px; - } - } - } - .order-right-page { - float: right; - width: 35%; - text-align: center; - - .order-total { - color: #ed3618; - } - } -} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/checkout_order.css.scss b/web/app/assets/stylesheets/client/checkout_order.css.scss new file mode 100644 index 000000000..9059198c4 --- /dev/null +++ b/web/app/assets/stylesheets/client/checkout_order.css.scss @@ -0,0 +1,204 @@ +@import "client/common.css.scss"; +#checkoutOrderScreen { + + + p { + font-size:12px; + margin:0; + } + + .payment-prompt { + color:white; + line-height:125%; + } + + h2 { + color:white; + background-color:#4d4d4d; + font-weight:normal; + margin: 0 0 10px 0; + font-size:14px; + padding: 3px 0 3px 10px; + height: 14px; + line-height: 14px; + vertical-align: middle; + text-align:left; + } + + + + .action-bar { + margin-top:20px; + } + + .line { + margin:10px 0 10px; + border-width:0 0 1px 0; + border-color:#ccc; + border-style:solid; + } + + #checkout-info-help { + margin-right:1px; + } + + .billing-info-item { + margin-bottom:3px; + } + + .country { + margin-left:15px; + } + .billing-address { + margin-bottom:20px; + } + .order-panel { + padding: 30px; + min-width:730px; + + .place-order { + font-size: 14px; + padding: 1px 3px; + line-height: 15px; + } + + .place-order-center { + text-align:center; + margin:20px 0 20px; + } + + .change-payment-info { + position:absolute; + font-size:12px; + left:180px; + } + + .billing-caption { + margin-bottom:5px; + float:left; + position:relative; + } + .order-header { + h2 { + font-size: 16px; + } + } + + .shipping-address { + display:none; + } + + .order-help { + margin:20px 0 30px; + } + .order-summary { + padding:0 20px; + + .billing-caption { + float:none; + margin-bottom:10px; + } + } + .order-items-header { + float:left; + margin-bottom:5px; + } + + .order-items-value { + float:right; + } + + .order-content { + margin-top: 20px; + background-color:#262626; + } + + .order-left-page { + float: left; + width: 65%; + background-color:#262626; + border-width:0 1px 0 0; + border-style:solid; + border-color:#333; + @include border_box_sizing; + + .payment-info-page { + + .info-caption-link { + .caption-text { + float: left; + } + .caption-link { + float: left; + margin-left: 5px; + } + } + + .address-info { + width: 50%; + float: left; + padding:0 10px; + @include border_box_sizing; + margin-bottom:30px; + } + + .payment-method-info { + width: 50%; + float: left; + padding:0 10px; + @include border_box_sizing; + } + } + .order-items-page { + .cart-item-caption { + width: 50%; + text-align: left; + float: left; + margin-bottom:10px; + @include border_box_sizing; + } + + .cart-item-price { + width: 25%; + text-align: right; + float: left; + padding:0 10px; + margin-bottom:10px; + @include border_box_sizing; + } + + .cart-item-quantity { + width: 10%; + text-align: right; + float: left; + padding:0 10px; + margin-bottom:10px; + @include border_box_sizing; + } + + .cart-items { + margin-top: 10px; + padding-left:10px; + } + + .cart-item { + margin-top: 10px; + } + + .no-cart-items { + } + } + } + .order-right-page { + float: left; + width: 35%; + text-align: left; + background-color:#262626; + @include border_box_sizing; + + .order-total { + color: #ed3618; + } + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/checkout_payment.css.scss b/web/app/assets/stylesheets/client/checkout_payment.css.scss index 0bec39083..97c17c359 100644 --- a/web/app/assets/stylesheets/client/checkout_payment.css.scss +++ b/web/app/assets/stylesheets/client/checkout_payment.css.scss @@ -3,6 +3,7 @@ .payment-wrapper { padding:10px 30px; + min-width:600px; } p { @@ -41,6 +42,12 @@ input[type="text"], input[type="password"] { width: 90%; + @include border_box_sizing; + } + + select#billing-country { + width:90%; + @include border_box_sizing; } &.signed-in { @@ -53,6 +60,21 @@ } } } + &.not-signed-in { + .row.second { + .left-side { + display:none; + } + .right-side { + width:100%; + } + } + } + + #divShippingFirstName, #divShippingLastName { + display:none; + } + .row { margin-top:20px; @@ -87,6 +109,23 @@ float: left; @include border_box_sizing; } + + div.terms-of-service.ichecbuttons { + margin-left:5px; + .icheckbox_minimal { + + float: left; + display: block; + margin: 5px 5px 0 0; + } + } + .terms-of-service-label-holder { + font-size:12px; + line-height:18px; + top:4px; + position:relative; + float:left; + } } .hint { @@ -131,11 +170,20 @@ @include border_box_sizing; } - .save-card-checkbox { + .save-card-checkbox, .reuse-existing-card-checkbox { float:left; display:block; margin-right:5px; } + + label[for="reuse-existing-card"], label[for="save-card"] { + line-height: 18px; + vertical-align: middle; + } + + .reuse-existing-card-helper { + margin-bottom:10px; + } } .shipping-address { diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 73fbaf592..4f94cb2b4 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -55,6 +55,7 @@ *= require ./checkout *= require ./checkout_signin *= require ./checkout_payment + *= require ./checkout_order *= require ./genreSelector *= require ./sessionList *= require ./searchResults diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index fcfca9f13..69f430b7f 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -12,6 +12,7 @@ $ColorLinkHover: #82AEAF; $ColorSidebarText: #a0b9bd; $ColorScreenBackground: lighten($ColorUIBackground, 10%); $ColorTextBoxBackground: #c5c5c5; +$ColorTextBoxDisabledBackground: #999; $ColorRecordingBackground: #471f18; $ColorTextHighlight: white; diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss index 9bee28a72..7fd3bd175 100644 --- a/web/app/assets/stylesheets/client/content.css.scss +++ b/web/app/assets/stylesheets/client/content.css.scss @@ -222,6 +222,10 @@ border:none; -webkit-box-shadow: inset 2px 2px 3px 0px #888; box-shadow: inset 2px 2px 3px 0px #888; + + &:disabled { + background-color: $ColorTextBoxDisabledBackground; + } } } diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index ab1444769..2aa254437 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -322,6 +322,10 @@ input[type="text"], input[type="password"]{ border:none; padding:3px; font-size:15px; + + &:disabled { + background-color: $ColorTextBoxDisabledBackground; + } } textarea { diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index d5b7fbaec..28ef551eb 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -8,10 +8,18 @@ class ApiRecurlyController < ApiController def create_account billing_info = params[:billing_info] + shipping_info = params[:shipping_info] + # should we let the user reuse this card next time? + reuse_card_next_time = params[:reuse_card_next_time] == "true" + # should we update the card info, or use what's on file this time? + reuse_card_this_time = params[:reuse_card_this_time] == "true" + # terms of service accepted? + terms_of_service = params[:terms_of_service] == "true" + if current_user # keep reuse card up-to-date - User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card]) + User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card_next_time]) else options = { remote_ip: request.remote_ip, @@ -20,17 +28,17 @@ class ApiRecurlyController < ApiController email: params[:email], password: params[:password], password_confirmation: params[:password], - terms_of_service: params[:terms_of_service], - instruments: [{ :instrument_id => 'other', :proficiency_level => 3, :priority => 1 }], + terms_of_service: terms_of_service, + instruments: [{ :instrument_id => 'other', :proficiency_level => 1, :priority => 1 }], birth_date: nil, location: { :country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]}, musician: true, - recaptcha_failed: false, + skip_recaptcha: true, invited_user: nil, fb_signup: nil, signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm", any_user: any_user, - reuse_card: params[:reuse_card] + reuse_card: reuse_card_next_time } user = UserManager.new.signup(options) @@ -45,7 +53,14 @@ class ApiRecurlyController < ApiController end begin - @account = @client.find_or_create_account(current_user, billing_info) + billing_info[:ip_address] = request.remote_ip if billing_info + if reuse_card_this_time + # do not attempt to update any billing/shipping info unless the user re-inputs their info! + @account = @client.get_account(current_user) + else + @account = @client.find_or_create_account(current_user, billing_info) + end + render :json=>account_json(@account) rescue RecurlyClientError => x render json: { :message => x.inspect, errors: x.errors }, :status => 404 @@ -61,7 +76,7 @@ class ApiRecurlyController < ApiController # get Recurly account def get_account - @account=@client.get_account(current_user) + @account = @client.get_account(current_user) render :json=>account_json(@account) rescue RecurlyClientError => e @@ -79,9 +94,11 @@ class ApiRecurlyController < ApiController # get Billing Information def billing_info @account = @client.get_account(current_user) - # @billing = @account.billing_info - # @billing ||= @account - render :json=> account_json(@account) + if @account + render :json=> account_json(@account) + else + render :json=> {}, :status => 404 + end rescue RecurlyClientError => x render json: { message: x.inspect, errors: x.errors}, :status => 404 end @@ -96,33 +113,20 @@ class ApiRecurlyController < ApiController def place_order error=nil - puts "PLACING ORDER #{params.inspect}" response = {jam_tracks:[]} - # 1st confirm that all specified JamTracks exist - jam_tracks = [] + current_user.shopping_carts.each do |shopping_cart| + jam_track = shopping_cart.cart_product - params[:jam_tracks].each do |jam_track_id| - jam_track = JamTrack.where("id=?", jam_track_id).first - if jam_track - jam_tracks << jam_track - else - error="JamTrack not found for '#{jam_track_id}'" - break - end + # if shopping_cart has any marked_for_redeem, then we zero out the price by passing in 'free' + # NOTE: shopping_carts have the idea of quantity, but you should only be able to buy at most one JamTrack. So anything > 0 is considered free for a JamTrack + + jam_track_right = @client.place_order(current_user, jam_track, shopping_cart) + # build up the response object with JamTracks that were purchased. + # if this gets more complicated, we should switch to RABL + response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version} end - # then buy each - unless error - jam_tracks.each do |jam_track| - jam_track_right = @client.place_order(current_user, jam_track) - # build up the response object with JamTracks that were purchased. - # if this gets more complicated, we should switch to RABL - response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version} - end - end - - if error render json: { errors: {message:error}}, :status => 404 else @@ -138,16 +142,21 @@ private end def account_json(account) + + billing_info = account.billing_info.nil? ? nil : { + :first_name => account.billing_info.first_name, + :last_name => account.billing_info.last_name, + :address1 => account.billing_info.address1, + :address2 => account.billing_info.address2, + :city => account.billing_info.city, + :state => account.billing_info.state, + :zip => account.billing_info.zip, + :country => account.billing_info.country, + :last_four => account.billing_info.last_four + } + { - :first_name => account.first_name, - :last_name => account.last_name, - :email => account.email, - :address1 => account.billing_info ? account.billing_info.address1 : nil, - :address2 => account.billing_info ? account.billing_info.address2 : nil, - :city => account.billing_info ? account.billing_info.city : nil, - :state => account.billing_info ? account.billing_info.state : nil, - :zip => account.billing_info ? account.billing_info.zip : nil, - :country => account.billing_info ? account.billing_info.country : nil + billing_info: billing_info } end diff --git a/web/app/controllers/api_shopping_carts_controller.rb b/web/app/controllers/api_shopping_carts_controller.rb index 3bb02701f..bfc3eb750 100644 --- a/web/app/controllers/api_shopping_carts_controller.rb +++ b/web/app/controllers/api_shopping_carts_controller.rb @@ -16,7 +16,11 @@ class ApiShoppingCartsController < ApiController raise StateError, "Invalid JamTrack." end - @cart = ShoppingCart.create any_user, jam_track + ShoppingCart.transaction do + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) + @cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) + end + if @cart.errors.any? response.status = :unprocessable_entity @@ -46,7 +50,21 @@ class ApiShoppingCartsController < ApiController @cart = any_user.shopping_carts.find_by_id(params[:id]) raise StateError, "Invalid Cart." if @cart.nil? - @cart.destroy + ShoppingCart.transaction do + @cart.destroy + + # check if we should move the redemption + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) + + carts = any_user.shopping_carts + + # if we find any carts on the account, mark one redeemable + if mark_redeem && carts.length > 0 + carts[0].redeem(mark_redeem) + carts[0].save + end + end + respond_with responder: ApiResponder, :status => 204 end diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index af67eb49b..5366cf42f 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -17,6 +17,10 @@ if @user == current_user geoiplocation.info if geoiplocation end + node :free_jamtrack do |user| + Rails.application.config.one_free_jamtrack_per_user && user.has_redeemable_jamtrack + end + node :mods do |user| user.mods_json end diff --git a/web/app/views/clients/_checkout_order.html.slim b/web/app/views/clients/_checkout_order.html.slim new file mode 100644 index 000000000..dbd944cce --- /dev/null +++ b/web/app/views/clients/_checkout_order.html.slim @@ -0,0 +1,154 @@ +div layout="screen" layout-id="checkoutOrder" id="checkoutOrderScreen" class="screen secondary" + .content + .content-head + .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) + h1 check out + = render "screen_navigation" + .content-body + #order_error.error.hidden + .content-body-scroller + .content-wrapper + .checkout-navigation-bar + .order-panel + .payment-wrapper + p.payment-prompt.hidden + | Please review your order, and if everything looks correct, click the PLACE YOUR ORDER button. Thank you! + p.empty-cart-prompt.hidden + | You have nothing in your cart. You can go browse for JamTracks  + a href="/client#/jamtrack" here + | . + .order-content + + + .clearall + .action-bar + + .right + a.button-grey href="#" id="checkout-info-help" HELP + a.button-grey.back href="#" BACK + a.button-orange.place-order href="#" PLACE YOUR ORDER + .clearall + .thanks-panel + h2 Thank you for your order! + br + .thanks-detail We'll send you an email confirming your order shortly. + br + .thanks-detail.jam-tracks-in-browser.hidden + | To play your purchased JamTrack, launch the JamKazam application and open the JamTrack while in a session. + .thanks-detail.purchased-jam-track.hidden + h2.purchased-jam-track-header Downloading Your Purchased JamTracks + span Each JamTrack will be downloaded sequentially. + br + span.notice Note that you do not have to wait for this to complete in order to use your JamTrack later. + br.clear + ul.purchased-list + + + +script type='text/template' id='template-order-content' + .order-left-page + .payment-info-page + h2 ADDRESS & PAYMENT + .address-info + .billing-address + .billing-caption + | BILLING ADDRESS: + a.change-payment-info href="#" change + .clearall + .billing-info-item= "{{data.billing_info.address1}}" + .billing-info-item= "{{data.billing_info.address2}}" + .billing-info-item + | {{data.billing_info.city}}, {{data.billing_info.state}} {{data.billing_info.zip}} + span.country= "{{data.billing_info.country}}" + .shipping-address + .billing-caption + | SHIPPING ADDRESS: + a.change-payment-info href="#" change + .clearall + = "{% if (data.shipping_as_billing) { %}" + .billing-info-item Same as billing address + = "{% } else { %}" + .billing-info-item= "{{data.shipping_info.address1}}" + .billing-info-item= "{{data.shipping_info.address2}}" + .billing-info-item + | {{data.shipping_info.city}}, {{data.shipping_info.state}} {{data.shipping_info.zip}} + span.country= "{{data.shipping_info.country}}" + = "{% } %}" + br + .payment-method-info + .billing-caption + | PAYMENT METHOD: + a.change-payment-info href="#" change + .clearall + + /= image_tag '' + ="Credit card ending {{data.billing_info.last_four}}" + + .clearall + .order-items-page + h2 ORDER DETAILS + .cart-items + .cart-item-caption + span YOUR ORDER INCLUDES: + .cart-item-price + span PRICE + .cart-item-quantity + span QUANTITY + .clearall + = "{% if (data.carts.length == 0) { %}" + .no-cart-items You have no orders now. + = "{% } %}" + = "{% _.each(data.carts, function(cart) { %}" + .cart-item cart-id="{{cart.id}}" + .cart-item-caption + = "{{cart.cart_type}}: {{cart.product_info.name}}" + .cart-item-price + = "$ {{Number(cart.product_info.total_price).toFixed(2)}}" + .cart-item-quantity + = "{{cart.quantity}}" + .clearall + = "{% }); %}" + .clearall + .order-right-page + h2 PLACE ORDER + .recurly-data.hidden + = "{% _.each(data.carts, function(cart) { %}" + .plan data-plan-code="{{cart.product_info.plan_code}}" + input data-recurly="plan" type="text" value="{{cart.product_info.plan_code}}" + = "{% }); %}" + .order-summary + .place-order-center + a.button-orange.place-order href="#" PLACE YOUR ORDER + .clearall + + .billing-caption ORDER SUMMARY: + .order-items-header Order items: + .order-items-value ${{data.sub_total}} + .clearall + .order-items-header Shipping & handling: + .order-items-value $0.00 + .clearall + .line + .order-items-header Total before tax: + .order-items-value ${{data.sub_total}} + .clearall + .order-items-header.taxes Taxes: + .order-items-value.taxes Calculating... + .clearall + .line + .order-items-header.order-total Order total: + .order-items-value.order-total Calculating... + .clearall + .order-help + span By placing your order, you agree to JamKazam's + ' + a href="http://www.jamkazam.com/corp/terms" rel="external" terms of service + ' + span and + ' + a href="http://www.jamkazam.com/corp/returns" rel="external" returns policy + span . + .clearall + +script type='text/template' id='template-purchased-jam-track' + li data-jam-track-id="{{data.jam_track_id}}" \ No newline at end of file diff --git a/web/app/views/clients/_checkout_payment.html.slim b/web/app/views/clients/_checkout_payment.html.slim index 2b8b3862f..3ea9cb831 100644 --- a/web/app/views/clients/_checkout_payment.html.slim +++ b/web/app/views/clients/_checkout_payment.html.slim @@ -1,20 +1,22 @@ -div layout="screen" layout-id="checkout_payment" id="checkoutPaymentScreen" class="screen secondary no-login-required" +div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class="screen secondary no-login-required" .content .content-head .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) h1 check out = render "screen_navigation" .content-body - #order_error.error.hidden + #payment_error.error.hidden .content-body-scroller .content-wrapper .checkout-navigation-bar .payment-wrapper - p.payment-prompt - | Please enter you billing address and payment information below. You will not be billed for your first JamTrack, which is 100% free.  + p.payment-prompt.free-jamtrack.hidden + | Please enter your billing address and payment information below. You will not be billed for your first JamTrack, which is 100% free.  | But we need this data to prevent fraud/abuse of those who would create multiple accounts to collect multiple free JamTracks.  | You will not be billed for any charges of any kind without your explicit authorization.  | There are no "hidden" charges or fees, thank you! + p.payment-prompt.no-free-jamtrack.hidden + | Please enter your billing address and payment information below.  form class="payment-info" id="checkout-payment-info" .row.first @@ -67,37 +69,49 @@ div layout="screen" layout-id="checkout_payment" id="checkoutPaymentScreen" clas .billing-label label for="billing-country" Country: .billing-value - input type="text" id="billing-country" + select id="billing-country" + option value="US" US .clearall .right-side .payment-method h2.payment-method-caption PAYMENT METHOD - #divCardName.field + .new-card-info + #divCardName.field.hidden + .card-label + label for="card-name" Name of Card: + .card-value + input type="text" id="card-name" + .clearall + #divCardNumber.field + .card-label + label for="card-number" Card Number: + .card-value + input type="text" id="card-number" + .clearall + #divCardExpiry.field + .card-label Expiration Date: + .card-value + =date_select("card", "expire-date", use_two_digit_numbers: true, discard_day: true, :start_year => Time.now.year, :end_year => Time.now.year + 18, :order => [:month, :day, :year], :default => -25.years.from_now, :html => {:class => "account-profile-birthdate", :id => "card-expiry"}) + .clearall + #divCardVerify.field + .card-label + label for="card-verify" + | CVV Code: + .hint.cvv + | (back of card) + .card-value + input type="text" id="card-verify" + .clearall + .reuse-existing-card .card-label - label for="card-name" Name of Card: .card-value - input type="text" id="card-name" - .clearall - #divCardNumber.field - .card-label - label for="card-number" Card Number: - .card-value - input type="text" id="card-number" - .clearall - #divCardExpiry.field - .card-label Expiration Date: - .card-value - =date_select("card", "expire-date", use_two_digit_numbers: true, discard_day: true, :start_year => Time.now.year, :end_year => Time.now.year + 18, :order => [:month, :day, :year], :default => -25.years.from_now, :html => {:class => "account-profile-birthdate", :id => "card-expiry"}) - .clearall - #divCardVerify.field - .card-label - label for="card-verify" - | CVV Code: - .hint.cvv - | (back-of-card) - .card-value - input type="text" id="card-verify" - .clearall + .reuse-existing-card-checkbox.ichecbuttons + input type="checkbox" id="reuse-existing-card" name="reuse-existing-card" checked="checked" + .reuse-existing-card-helper + label for="reuse-existing-card" + | Use card ending in  + span.existing-card-ends-with + .clearall .card-label .card-value .save-card-checkbox.ichecbuttons @@ -108,7 +122,7 @@ div layout="screen" layout-id="checkout_payment" id="checkoutPaymentScreen" clas .clearall .clearall .row.second - .left-side + left-side.hidden .shipping-address h2.shipping-address-label SHIPPING ADDRESS .shipping-as-billing.ichecbuttons @@ -180,6 +194,14 @@ div layout="screen" layout-id="checkout_payment" id="checkoutPaymentScreen" clas .account-value input name="password" type="password" .clearall + #divJamKazamTos.field + .terms-of-service.ichecbuttons + input type="checkbox" name="terms-of-service" + .terms-of-service-label-holder + label for="terms-of-service" + | I have read and agree to the JamKazam  + a rel="external" href=corp_terms_path terms of service + .clearall .clearall diff --git a/web/app/views/clients/_checkout_signin.html.slim b/web/app/views/clients/_checkout_signin.html.slim index e0f089326..d6dd4ccdb 100644 --- a/web/app/views/clients/_checkout_signin.html.slim +++ b/web/app/views/clients/_checkout_signin.html.slim @@ -1,4 +1,4 @@ -div layout="screen" layout-id="checkout_signin" id="checkoutSignInScreen" class="screen secondary no-login-required" +div layout="screen" layout-id="checkoutSignin" id="checkoutSignInScreen" class="screen secondary no-login-required" .content .content-head .content-icon= image_tag("content/icon_shopping_cart.png", {:height => 19, :width => 19}) diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 82e1d18ba..5c426bc1a 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -41,6 +41,7 @@ <%= render "shopping_cart" %> <%= render "checkout_signin" %> <%= render "checkout_payment" %> +<%= render "checkout_order" %> <%= render "order" %> <%= render "feed" %> <%= render "bands" %> @@ -241,7 +242,10 @@ checkoutSignInScreen.initialize(); var checkoutPaymentScreen = new JK.CheckoutPaymentScreen(JK.app); - checkoutPaymentScreen.initialize(); + checkoutPaymentScreen.initialize(); + + var checkoutOrderScreen = new JK.CheckoutOrderScreen(JK.app); + checkoutOrderScreen.initialize(); // var OrderScreen = new JK.OrderScreen(JK.app); // OrderScreen.initialize(); diff --git a/web/app/views/layouts/client.html.erb b/web/app/views/layouts/client.html.erb index 2c4238803..adee2a9d7 100644 --- a/web/app/views/layouts/client.html.erb +++ b/web/app/views/layouts/client.html.erb @@ -35,5 +35,7 @@ <%= yield %> <%= render "shared/ga" %> + <%= render "shared/recurly" %> + diff --git a/web/app/views/shared/_recurly.html.slim b/web/app/views/shared/_recurly.html.slim new file mode 100644 index 000000000..d22bdabbd --- /dev/null +++ b/web/app/views/shared/_recurly.html.slim @@ -0,0 +1,4 @@ +script src="https://js.recurly.com/v3/recurly.js" + +javascript: + recurly.configure(gon.global.recurly_public_api_key) \ No newline at end of file diff --git a/web/config/application.rb b/web/config/application.rb index d594b65ca..1ff3ea9d7 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -318,5 +318,6 @@ if defined?(Bundler) config.show_jamblaster_kickstarter_link = true config.metronome_available = true config.backing_tracks_available = true + config.one_free_jamtrack_per_user = true end end diff --git a/web/config/initializers/gon.rb b/web/config/initializers/gon.rb index 228bff6b1..ed4f92a4f 100644 --- a/web/config/initializers/gon.rb +++ b/web/config/initializers/gon.rb @@ -10,4 +10,6 @@ Gon.global.influxdb_port = Rails.application.config.influxdb_port Gon.global.influxdb_database = Rails.application.config.influxdb_database Gon.global.influxdb_username = Rails.application.config.influxdb_unsafe_username Gon.global.influxdb_password = Rails.application.config.influxdb_unsafe_password +Gon.global.recurly_public_api_key = Rails.application.config.recurly_public_api_key +Gon.global.one_free_jamtrack_per_user = Rails.application.config.one_free_jamtrack_per_user Gon.global.env = Rails.env diff --git a/web/lib/user_manager.rb b/web/lib/user_manager.rb index 5c36c6974..75ce77b07 100644 --- a/web/lib/user_manager.rb +++ b/web/lib/user_manager.rb @@ -26,8 +26,12 @@ class UserManager < BaseManager fb_signup = options[:fb_signup] signup_confirm_url = options[:signup_confirm_url] affiliate_referral_id = options[:affiliate_referral_id] - recaptcha_failed=(fb_signup) ? false : !@google_client.verify_recaptcha(options[:recaptcha_response]) - + any_user = options[:any_user] + recaptcha_failed = false + unless options[:skip_recaptcha] # allow callers to opt-of recaptcha + recaptcha_failed = fb_signup ? false : !@google_client.verify_recaptcha(options[:recaptcha_response]) + end + user = User.new # check if we have disabled open signup for this site. open == invited users can still get in @@ -62,11 +66,9 @@ class UserManager < BaseManager invited_user: invited_user, fb_signup: fb_signup, signup_confirm_url: signup_confirm_url, - affiliate_referral_id: affiliate_referral_id) - - - return user - #end + affiliate_referral_id: affiliate_referral_id, + any_user: any_user) + user end def signup_confirm(signup_token, remote_ip=nil) diff --git a/web/spec/controllers/api_recurly_spec.rb b/web/spec/controllers/api_recurly_spec.rb index fb0eeabfd..bc423f899 100644 --- a/web/spec/controllers/api_recurly_spec.rb +++ b/web/spec/controllers/api_recurly_spec.rb @@ -35,7 +35,7 @@ describe ApiRecurlyController, :type=>:controller do it "should send correct error" do @billing_info[:number]='121' - post :create_account, {:format => 'json', :billing_info=>@billing_info} + post :create_account, {:format => 'json', :billing_info=>@billing_info, reuse_card_this_time: false, reuse_card_next_time: false} response.status.should == 404 body = JSON.parse(response.body) body['errors'].should have(1).items @@ -43,36 +43,39 @@ describe ApiRecurlyController, :type=>:controller do end it "should create account" do - post :create_account, {:format => 'json'} - response.should be_success + post :create_account, {:format => 'json',billing_info: @billing_info, reuse_card_this_time: false, reuse_card_next_time: false} + response.should be_success + body = JSON.parse(response.body) + response.should be_success + body['billing_info']['first_name'].should eq(@user.first_name) end - it "should retrieve account" do - post :create_account, {:format => 'json'} + it "should retrieve account with no billing info" do + post :create_account, {:format => 'json', reuse_card_this_time: false, reuse_card_next_time: false} response.should be_success get :get_account body = JSON.parse(response.body) response.should be_success - body['email'].should eq(@user.email) + body['billing_info'].should be_nil end it "should update account" do - post :create_account + post :create_account, {:format => 'json', billing_info: @billing_info, reuse_card_this_time: false, reuse_card_next_time: false} response.should be_success body = JSON.parse(response.body) - body['first_name'].should eq("Person") + body['billing_info']['first_name'].should eq("Person") - @user.update_attribute(:first_name, "Thing") controller.current_user = @user - put :update_account + @billing_info[:first_name] = "Thing" + put :update_account, {:format => 'json', billing_info: @billing_info} body = JSON.parse(response.body) - body['first_name'].should eq("Thing") + body['billing_info']['first_name'].should eq("Thing") get :get_account, { :format => 'json'} response.should be_success body = JSON.parse(response.body) - body['first_name'].should eq("Thing") + body['billing_info']['first_name'].should eq("Thing") end # Note: We don't have any subscriptions yet: @@ -95,13 +98,13 @@ describe ApiRecurlyController, :type=>:controller do # $enable_tracing = true - post :create_account + post :create_account, {:format => 'json', billing_info: @billing_info, reuse_card_this_time: false, reuse_card_next_time: false} response.should be_success body = JSON.parse(response.body) - body['first_name'].should eq("Person") + body['billing_info']['first_name'].should eq("Person") @billing_info[:state] = "NE" - put :update_billing_info, {:format => 'json', :billing_info=>@billing_info} + put :update_billing_info, {format: 'json', billing_info: @billing_info} response.should be_success body = JSON.parse(response.body) diff --git a/web/spec/factories.rb b/web/spec/factories.rb index af9c1e1c7..d625ec53c 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -20,7 +20,7 @@ FactoryGirl.define do terms_of_service true subscribe_email true last_jam_audio_latency 5 - resue_card true + reuse_card true factory :fan do musician false diff --git a/web/spec/features/checkout_spec.rb b/web/spec/features/checkout_spec.rb new file mode 100644 index 000000000..8e44497b4 --- /dev/null +++ b/web/spec/features/checkout_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe "Checkout", :js => true, :type => :feature, :capybara_feature => true do + + let(:user) { FactoryGirl.create(:user) } + let(:jamtrack_acdc_backinblack) { FactoryGirl.create(:jam_track, name: 'Back in Black', original_artist: 'AC/DC', sales_region: 'United States', make_track: true, plan_code: 'jamtrack-acdc-backinblack') } + + before(:all) do + Capybara.javascript_driver = :poltergeist + Capybara.current_driver = Capybara.javascript_driver + Capybara.default_wait_time = 30 # these tests are SLOOOOOW + end + + + before(:each) do + ShoppingCart.delete_all + JamTrack.delete_all + JamTrackTrack.delete_all + JamTrackLicensor.delete_all + + stub_const("APP_CONFIG", web_config) + end + + + describe "Shopping" do + + before(:each) do + visit "/client#/jamtrack" + find('h1', text: 'jamtracks') + find('a', text: 'What is a JamTrack?') + + jk_select('Any', '#jamtrack-find-form #jamtrack_availability') + end + + it "shows all JamTracks" do + find_jamtrack jt_us + find_jamtrack jt_ww + find_jamtrack jt_rock + end + + it "filters with availability" do + jk_select('Worldwide', '#jamtrack-find-form #jamtrack_availability') + find_jamtrack jt_ww + not_find_jamtrack jt_us + not_find_jamtrack jt_rock + end + + it "filters with genre" do + jk_select('Blues', '#jamtrack-find-form #jamtrack_genre') + find_jamtrack jt_blues + not_find_jamtrack jt_rock + not_find_jamtrack jt_us + not_find_jamtrack jt_ww + end + + it "filters with instrument" do + jk_select('Electric Guitar', '#jamtrack-find-form #jamtrack_instrument') + find_jamtrack jt_us + find_jamtrack jt_ww + find_jamtrack jt_rock + end + + end + + describe "Shopping Carts" do + + before(:each) do + visit "/client#/jamtrack" + find('h1', text: 'jamtracks') + find('a', text: 'What is a JamTrack?') + + jk_select('Any', '#jamtrack-find-form #jamtrack_availability') + end + + it "adds/deletes JamTrack to/from Cart" do + find("a.jamtrack-add-cart[data-jamtrack-id=\"#{jt_us.id}\"]").trigger(:click) + + find('h1', text: 'shopping cart') + find('.cart-item-caption', text: "JamTrack: #{jt_us.name}") + find('.cart-item-price', text: "$ #{jt_us.price}") + + find('a.button-orange', text: 'CONTINUE SHOPPING').trigger(:click) + jk_select('Any', '#jamtrack-find-form #jamtrack_availability') + find_jamtrack jt_us, {added_cart: true} + + find('a.header-shopping-cart').trigger(:click) + find("a.remove-cart").trigger(:click) + find('a.button-orange', text: 'CONTINUE SHOPPING').trigger(:click) + jk_select('Any', '#jamtrack-find-form #jamtrack_availability') + + find_jamtrack jt_us + + find("a.jamtrack-add-cart[data-jamtrack-id=\"#{jt_us.id}\"]").trigger(:click) + find('a.button-orange', text: 'CONTINUE SHOPPING').trigger(:click) + find("a.jamtrack-add-cart[data-jamtrack-id=\"#{jt_ww.id}\"]").trigger(:click) + find('a.button-orange', text: 'CONTINUE SHOPPING').trigger(:click) + + find('.shopping-sub-total', text: "Subtotal: $ #{jt_us.price + jt_ww.price}") + end + + end +end diff --git a/web/spec/support/app_config.rb b/web/spec/support/app_config.rb index 0a7c63cae..3287dd0be 100644 --- a/web/spec/support/app_config.rb +++ b/web/spec/support/app_config.rb @@ -78,6 +78,10 @@ def web_config def signing_job_queue_max_time 20 # 20 seconds end + + def one_free_jamtrack_per_user + true + end end klass.new end diff --git a/websocket-gateway/spec/factories.rb b/websocket-gateway/spec/factories.rb index fb4307c47..ed446a48f 100644 --- a/websocket-gateway/spec/factories.rb +++ b/websocket-gateway/spec/factories.rb @@ -11,7 +11,7 @@ FactoryGirl.define do state "NC" country "US" terms_of_service true - resue_card true + reuse_card true factory :admin do