diff --git a/db/manifest b/db/manifest index 223d8cc39..48ba477b9 100755 --- a/db/manifest +++ b/db/manifest @@ -283,3 +283,4 @@ payment_history.sql jam_track_right_private_key.sql first_downloaded_jamtrack_at.sql signing.sql +optimized_redeemption.sql \ No newline at end of file diff --git a/db/up/optimized_redeemption.sql b/db/up/optimized_redeemption.sql new file mode 100644 index 000000000..476fef48d --- /dev/null +++ b/db/up/optimized_redeemption.sql @@ -0,0 +1,13 @@ +CREATE TABLE machine_fingerprints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + fingerprint VARCHAR(20000) NOT NULL UNIQUE, + when_taken VARCHAR NOT NULL, + print_type VARCHAR NOT NULL, + remote_ip VARCHAR(1000) NOT NULL, + jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE jam_track_rights ADD COLUMN redeemed_and_fingerprinted BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index f6fac2e21..09b125e3d 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -103,6 +103,7 @@ require "jam_ruby/models/genre" require "jam_ruby/models/user" require "jam_ruby/models/anonymous_user" require "jam_ruby/models/signup_hint" +require "jam_ruby/models/machine_fingerprint" require "jam_ruby/models/rsvp_request" require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_request_rsvp_slot" diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index ce9768b9a..182157e23 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -774,6 +774,7 @@ module JamRuby self.opening_jam_track = true self.save self.opening_jam_track = false + #self.tick_track_changes end def close_jam_track diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index 3d3d6e656..ff7b60730 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -4,10 +4,11 @@ module JamRuby class AnonymousUser - attr_accessor :id + attr_accessor :id, :cookies - def initialize(id) + def initialize(id, cookies) @id = id + @cookies = cookies end def shopping_carts @@ -23,7 +24,11 @@ module JamRuby end def has_redeemable_jamtrack - APP_CONFIG.one_free_jamtrack_per_user + APP_CONFIG.one_free_jamtrack_per_user && !@cookies[:redeemed_jamtrack] + end + + def signup_hint + SignupHint.find_by_anonymous_user_id(@id) end end end diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index 9150786ab..98ea69a51 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -3,6 +3,9 @@ module JamRuby # describes what users have rights to which tracks class JamTrackRight < ActiveRecord::Base include JamRuby::S3ManagerMixin + + @@log = Logging.logger[JamTrackRight] + attr_accessible :user, :jam_track, :user_id, :jam_track_id, :download_count attr_accessible :user_id, :jam_track_id, as: :admin attr_accessible :url_48, :md5_48, :length_48, :url_44, :md5_44, :length_44 @@ -211,6 +214,76 @@ module JamRuby .where('jam_tracks.id IN (?)', jamtracks) end + def guard_against_fraud(current_user, fingerprint, remote_ip) + if current_user.blank? + return "no user specified" + end + + # admin's get to skip fraud check + if current_user.admin + return nil + end + + if fingerprint.nil? || fingerprint.empty? + return "no fingerprint specified" + end + + all_fingerprint = fingerprint[:all] + running_fingerprint = fingerprint[:running] + + if all_fingerprint.blank? + return "no all fingerprint specified" + end + + if running_fingerprint.blank? + return "no running fingerprint specified" + end + + if redeemed && !redeemed_and_fingerprinted + # if this is a free JamTrack, we need to check for fraud or accidental misuse + + # first of all, does this user have any other JamTracks aside from this one that have already been redeemed it and are marked free? + other_redeemed_freebie = JamTrackRight.where(redeemed:true).where(redeemed_and_fingerprinted: true).where('id != ?', id).where(user_id: current_user.id).first + + if other_redeemed_freebie + return "already redeemed another" + end + + # can we find a jam track that belongs to someone else with the same fingerprint + match = MachineFingerprint.find_by_fingerprint(all_fingerprint) + + if match && match.user != current_user + AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}", + body: "MachineFingerprint #{match.inspect}\n\nCurrent User: #{current_user.admin_url}") + + # try to record the other fingerprint + MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, self) + return "other user has 'all' fingerprint" + end + + match = MachineFingerprint.find_by_fingerprint(running_fingerprint) + + if match && match.user != current_user + AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}", + body: "MachineFingerprint #{match.inspect}\n\nCurrent User: #{current_user.admin_url}") + # try to record the other fingerprint + MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, self) + return "other user has 'running' fingerprint" + end + + # we made it past all checks; let's slap on the redeemed_fingerprint + self.redeemed_and_fingerprinted = true + + MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, self) + MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, self) + + save! + end + + + nil + end + def self.stats stats = {} diff --git a/ruby/lib/jam_ruby/models/machine_fingerprint.rb b/ruby/lib/jam_ruby/models/machine_fingerprint.rb new file mode 100644 index 000000000..7d0076e50 --- /dev/null +++ b/ruby/lib/jam_ruby/models/machine_fingerprint.rb @@ -0,0 +1,35 @@ +module JamRuby + class MachineFingerprint < ActiveRecord::Base + + @@log = Logging.logger[MachineFingerprint] + + belongs_to :user, :class_name => "JamRuby::User" + belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight" + + TAKEN_ON_SUCCESSFUL_DOWNLOAD = 'dl' + TAKEN_ON_FRAUD_CONFLICT = 'fc' + + PRINT_TYPE_ALL = 'a' + PRINT_TYPE_ACTIVE = 'r' + + + validates :user, presence:true + validates :when_taken, :inclusion => {:in => [TAKEN_ON_SUCCESSFUL_DOWNLOAD, TAKEN_ON_FRAUD_CONFLICT]} + validates :fingerprint, presence: true, uniqueness:true + validates :print_type, presence: true, :inclusion => {:in =>[PRINT_TYPE_ALL, PRINT_TYPE_ACTIVE]} + validates :remote_ip, presence: true + + def self.create(fingerprint, user, when_taken, print_type, remote_ip, jam_track_right = nil) + mf = MachineFingerprint.new + mf.fingerprint = fingerprint + mf.user = user + mf.when_taken = when_taken + mf.print_type = print_type + mf.remote_ip = remote_ip + mf.jam_track_right = jam_track_right + unless mf.save + @@log.error("unable to create machine fingerprint: #{mf.errors.inspect}") + end + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 732bd45bb..bca02f526 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -128,6 +128,10 @@ module JamRuby # just a pain to implement end + def self.is_only_freebie(shopping_carts_jam_tracks) + shopping_carts_jam_tracks.length == 1 && shopping_carts_jam_tracks[0].product_info[:free] + end + # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned def self.order_jam_tracks(current_user, shopping_carts_jam_tracks) @@ -139,58 +143,76 @@ module JamRuby sale = create_jam_track_sale(current_user) if sale.valid? - account = client.get_account(current_user) - if account.present? + if is_only_freebie(shopping_carts_jam_tracks) + sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, nil) - purge_pending_adjustments(account) + sale.recurly_subtotal_in_cents = 0 + sale.recurly_tax_in_cents = 0 + sale.recurly_total_in_cents = 0 + sale.recurly_currency = 'USD' - created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents = 0 + sale_line_item.recurly_total_in_cents = 0 + sale_line_item.recurly_currency = 'USD' + sale_line_item.recurly_discount_in_cents = 0 + sale.save - # now invoice the sale ... almost done + else - begin - invoice = account.invoice! - sale.recurly_invoice_id = invoice.uuid - sale.recurly_invoice_number = invoice.invoice_number + account = client.get_account(current_user) + if account.present? - # now slap in all the real tax/purchase totals - sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents - sale.recurly_tax_in_cents = invoice.tax_in_cents - sale.recurly_total_in_cents = invoice.total_in_cents - sale.recurly_currency = invoice.currency + purge_pending_adjustments(account) + + created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account) + + # now invoice the sale ... almost done + + begin + invoice = account.invoice! + sale.recurly_invoice_id = invoice.uuid + sale.recurly_invoice_number = invoice.invoice_number + + # now slap in all the real tax/purchase totals + sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents + sale.recurly_tax_in_cents = invoice.tax_in_cents + sale.recurly_total_in_cents = invoice.total_in_cents + sale.recurly_currency = invoice.currency + + # and resolve against sale_line_items + sale.sale_line_items.each do |sale_line_item| + found_line_item = false + invoice.line_items.each do |line_item| + if line_item.uuid == sale_line_item.recurly_adjustment_uuid + sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents + sale_line_item.recurly_total_in_cents =line_item.total_in_cents + sale_line_item.recurly_currency = line_item.currency + sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents + found_line_item = true + break + end - # and resolve against sale_line_items - sale.sale_line_items.each do |sale_line_item| - found_line_item = false - invoice.line_items.each do |line_item| - if line_item.uuid == sale_line_item.recurly_adjustment_uuid - sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents - sale_line_item.recurly_total_in_cents =line_item.total_in_cents - sale_line_item.recurly_currency = line_item.currency - sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents - found_line_item = true - break end + if !found_line_item + @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}") + puts "CANT FIND LINE ITEM" + end end - if !found_line_item - @@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}") - puts "CANT FIND LINE ITEM" + unless sale.save + raise RecurlyClientError, "Invalid sale (at end)." end + rescue Recurly::Resource::Invalid => e + # this exception is thrown by invoice! if the invoice is invalid + sale.rollback_adjustments(current_user, created_adjustments) + sale = nil + raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic end - - unless sale.save - raise RecurlyClientError, "Invalid sale (at end)." - end - rescue Recurly::Resource::Invalid => e - # this exception is thrown by invoice! if the invoice is invalid - sale.rollback_adjustments(current_user, created_adjustments) - sale = nil - raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic + else + raise RecurlyClientError, "Could not find account to place order." end - else - raise RecurlyClientError, "Could not find account to place order." end else raise RecurlyClientError, "Invalid sale." @@ -238,30 +260,33 @@ module JamRuby return end - # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack - adjustments = shopping_cart.create_adjustment_attributes(current_user) + if account + # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack + adjustments = shopping_cart.create_adjustment_attributes(current_user) - adjustments.each do |adjustment| + adjustments.each do |adjustment| - # create the adjustment at Recurly (this may not look like it, but it is a REST API) - created_adjustment = account.adjustments.new(adjustment) - created_adjustment.save + # create the adjustment at Recurly (this may not look like it, but it is a REST API) + created_adjustment = account.adjustments.new(adjustment) + created_adjustment.save - # if the adjustment could not be made, bail - raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any? + # if the adjustment could not be made, bail + raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any? - # keep track of adjustments we created for this order, in case we have to roll them back - created_adjustments << created_adjustment + # keep track of adjustments we created for this order, in case we have to roll them back + created_adjustments << created_adjustment - if ShoppingCart.is_product_purchase?(adjustment) - # this was a normal product adjustment, so track it as such - recurly_adjustment_uuid = created_adjustment.uuid - else - # this was a 'credit' adjustment, so track it as such - recurly_adjustment_credit_uuid = created_adjustment.uuid + if ShoppingCart.is_product_purchase?(adjustment) + # this was a normal product adjustment, so track it as such + recurly_adjustment_uuid = created_adjustment.uuid + else + # this was a 'credit' adjustment, so track it as such + recurly_adjustment_credit_uuid = created_adjustment.uuid + end end end + # create one sale line item for every jam track sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid) @@ -279,7 +304,11 @@ module JamRuby end # also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks - User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if shopping_cart.free? + if shopping_cart.free? + User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) + current_user.has_redeemable_jamtrack = false # make sure model reflects the truth + end + # this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid diff --git a/ruby/lib/jam_ruby/models/shopping_cart.rb b/ruby/lib/jam_ruby/models/shopping_cart.rb index dc975bd40..568d94048 100644 --- a/ruby/lib/jam_ruby/models/shopping_cart.rb +++ b/ruby/lib/jam_ruby/models/shopping_cart.rb @@ -152,6 +152,12 @@ module JamRuby def self.add_jam_track_to_cart(any_user, jam_track) cart = nil ShoppingCart.transaction do + + if any_user.has_redeemable_jamtrack + # if you still have a freebie available to you, or if you are an anonymous user, we make sure there is nothing else in your shopping cart + any_user.destroy_all_shopping_carts + end + mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user) cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) end @@ -173,7 +179,13 @@ module JamRuby carts[0].save end end + end + def port(user, anonymous_user) + + ShoppingCart.transaction do + move_to_user(user, anonymous_user, anonymous_user.shopping_carts) + end end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/signup_hint.rb b/ruby/lib/jam_ruby/models/signup_hint.rb index b415d45a4..c95353d6c 100644 --- a/ruby/lib/jam_ruby/models/signup_hint.rb +++ b/ruby/lib/jam_ruby/models/signup_hint.rb @@ -6,6 +6,7 @@ module JamRuby # we use it to figure out what to do with the user after they signup class SignupHint < ActiveRecord::Base + belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :user, class_name: 'JamRuby::User' @@ -23,6 +24,7 @@ module JamRuby hint.anonymous_user_id = anonymous_user.id hint.redirect_location = options[:redirect_location] if options.has_key?(:redirect_location) hint.want_jamblaster = options[:want_jamblaster] if options.has_key?(:want_jamblaster) + #hint.jam_track = JamTrack.find(options[:jam_track]) if options.has_key?(:jam_track) hint.expires_at = 15.minutes.from_now hint.save hint diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 618c5818b..fcdeffc3c 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -953,6 +953,7 @@ module JamRuby recaptcha_failed = options[:recaptcha_failed] any_user = options[:any_user] reuse_card = options[:reuse_card] + signup_hint = options[:signup_hint] user = User.new @@ -1018,8 +1019,6 @@ module JamRuby end end - - unless fb_signup.nil? user.update_fb_authorization(fb_signup) @@ -1072,6 +1071,18 @@ module JamRuby user.save + # if the user has just one, free jamtrack in their shopping cart, and it matches the signup hint, then auto-buy it + # only_freebie_in_cart = + # signup_hint && + # signup_hint.jam_track && + # user.shopping_carts.length == 1 && + # user.shopping_carts[0].cart_product == signup_hint.jam_track && + # user.shopping_carts[0].product_info[:free] + # + # if only_freebie_in_cart + # Sale.place_order(user, user.shopping_carts) + # end + user.errors.add("recaptcha", "verification failed") if recaptcha_failed if user.errors.any? @@ -1577,6 +1588,21 @@ module JamRuby APP_CONFIG.admin_root_url + "/admin/jam_track_rights?q[user_id_equals]=#{id}&commit=Filter&order=created_at DESC" end + # these are signup attributes that we default to when not presenting the typical form @ /signup + def self.musician_defaults(remote_ip, confirmation_url, any_user, options) + options = options || {} + options[:remote_ip] = remote_ip + options[:birth_date] = nil + options[:instruments] = [{:instrument_id => 'other', :proficiency_level => 1, :priority => 1}] + options[:musician] = true + options[:skip_recaptcha] = true + options[:invited_user] = nil + options[:fb_signup] = nil + options[:signup_confirm_url] = confirmation_url + options[:any_user] = any_user + options + end + private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb index 1c68e2037..1adef7d18 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -208,5 +208,125 @@ describe JamTrackRight do end end + describe "guard_against_fraud" do + let(:user) {FactoryGirl.create(:user)} + let(:other) {FactoryGirl.create(:user)} + let(:first_fingerprint) { {all: 'all', running: 'running' } } + let(:new_fingerprint) { {all: 'all_2', running: 'running' } } + let(:remote_ip) {'1.1.1.1'} + let(:jam_track_right) { FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) } + let(:jam_track_right_purchased) { FactoryGirl.create(:jam_track_right, user: user, redeemed: false, redeemed_and_fingerprinted: false) } + let(:jam_track_right_other) { FactoryGirl.create(:jam_track_right, user: other, redeemed: true, redeemed_and_fingerprinted: false) } + let(:jam_track_right_other_purchased) { FactoryGirl.create(:jam_track_right, user: other, redeemed: false, redeemed_and_fingerprinted: false) } + + it "denies no current_user" do + jam_track_right.guard_against_fraud(nil, first_fingerprint, remote_ip).should eq('no user specified') + end + + it "denies no fingerprint" do + jam_track_right.guard_against_fraud(user, nil, remote_ip).should eq('no fingerprint specified') + end + + it "allows redemption (success)" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + jam_track_right.valid?.should be_true + jam_track_right.redeemed_and_fingerprinted.should be_true + + + mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:all]) + mf.user.should eq(user) + mf.fingerprint.should eq(first_fingerprint[:all]) + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right) + + mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:running]) + mf.user.should eq(user) + mf.fingerprint.should eq(first_fingerprint[:running]) + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) + mf.jam_track_right.should eq(jam_track_right) + end + + it "ignores already successfully redeemed" do + jam_track_right.redeemed_and_fingerprinted = true + jam_track_right.save! + + jam_track_right.guard_against_fraud(user, new_fingerprint, remote_ip).should be_nil + jam_track_right.valid?.should be_true + + # and no new fingerprints + MachineFingerprint.count.should eq(0) + end + + it "ignores already normally purchased" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip) + MachineFingerprint.count.should eq(2) + + jam_track_right_purchased.guard_against_fraud(user, new_fingerprint, remote_ip).should be_nil + jam_track_right_purchased.valid?.should be_true + jam_track_right_purchased.redeemed_and_fingerprinted.should be_false # fingerprint should not be set on normal purchase + + jam_track_right.redeemed_and_fingerprinted.should be_true # should still be redeemed_and fingerprinted; just checking for weird side-effects + + # no new fingerprints + MachineFingerprint.count.should eq(2) + end + + it "protects against re-using fingerprint across users (conflicts on all fp)" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint[:running] = 'running_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint, remote_ip).should eq("other user has 'all' fingerprint") + + mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:running]) + mf.user.should eq(other) + mf.fingerprint.should eq(first_fingerprint[:running]) + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) + mf.jam_track_right.should eq(jam_track_right_other) + MachineFingerprint.count.should eq(3) + end + + it "protects against re-using fingerprint across users (conflicts on running fp)" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint, remote_ip).should eq("other user has 'running' fingerprint") + + mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:all]) + mf.user.should eq(other) + mf.fingerprint.should eq(first_fingerprint[:all]) + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right_other) + MachineFingerprint.count.should eq(3) + end + + # if you try to buy a regular jamtrack with a fingerprint belonging to another user? so what. you paid for it + it "allows re-use of fingerprint if jamtrack is a normal purchase" do + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + jam_track_right_other_purchased.guard_against_fraud(other, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + end + + it "stops you from redeeming two jamtracks" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: true) + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should eq('already redeemed another') + MachineFingerprint.count.should eq(0) + end + + it "let's you download a free jamtrack if you have a second but undownloaded free one" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + + right1.guard_against_fraud(user, first_fingerprint, remote_ip).should eq('already redeemed another') + MachineFingerprint.count.should eq(2) + end + + end + end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index b08aad36d..47dda7213 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -87,9 +87,9 @@ describe Sale do sales.should eq(user.sales) sale = sales[0] - sale.recurly_invoice_id.should_not be_nil + sale.recurly_invoice_id.should be_nil - sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_subtotal_in_cents.should eq(0) sale.recurly_tax_in_cents.should eq(0) sale.recurly_total_in_cents.should eq(0) sale.recurly_currency.should eq('USD') @@ -97,7 +97,7 @@ describe Sale do sale.sale_line_items.length.should == 1 sale_line_item = sale.sale_line_items[0] sale_line_item.recurly_tax_in_cents.should eq(0) - sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale_line_item.recurly_total_in_cents.should eq(0) sale_line_item.recurly_currency.should eq('USD') sale_line_item.recurly_discount_in_cents.should eq(0) sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) @@ -109,8 +109,8 @@ describe Sale do sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) sale_line_item.product_id.should eq(jamtrack.id) sale_line_item.recurly_subscription_uuid.should be_nil - sale_line_item.recurly_adjustment_uuid.should_not be_nil - sale_line_item.recurly_adjustment_credit_uuid.should_not be_nil + sale_line_item.recurly_adjustment_uuid.should be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid) @@ -118,31 +118,11 @@ describe Sale do recurly_account = client.get_account(user) adjustments = recurly_account.adjustments adjustments.should_not be_nil - adjustments.should have(2).items - free_purchase= adjustments[0] - free_purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) - free_purchase.accounting_code.should eq(ShoppingCart::PURCHASE_FREE) - free_purchase.description.should eq("JamTrack: " + jamtrack.name) - free_purchase.state.should eq('invoiced') - free_purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) - - free_credit = adjustments[1] - free_credit.unit_amount_in_cents.should eq(-(jamtrack.price * 100).to_i) - free_credit.accounting_code.should eq(ShoppingCart::PURCHASE_FREE_CREDIT) - free_credit.description.should eq("JamTrack: " + jamtrack.name + " (Credit)") - free_credit.state.should eq('invoiced') - free_credit.uuid.should eq(sale_line_item.recurly_adjustment_credit_uuid) + adjustments.should have(0).items invoices = recurly_account.invoices - invoices.should have(1).items - invoice = invoices[0] - invoice.uuid.should eq(sale.recurly_invoice_id) - invoice.line_items.should have(2).items # should have both adjustments associated - invoice.line_items[0].should eq(free_credit) - invoice.line_items[1].should eq(free_purchase) - invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i) - invoice.total_in_cents.should eq(0) - invoice.state.should eq('collected') + invoices.should have(0).items + # verify jam_track_rights data user.jam_track_rights.should_not be_nil @@ -238,7 +218,7 @@ describe Sale do # also, verify that no earlier adjustments were affected recurly_account = client.get_account(user) adjustments = recurly_account.adjustments - adjustments.should have(2).items + adjustments.should have(0).items # because the only successful purchase was a freebie, there should be no recurly adjustments end # this test counts on the fact that two adjustments are made when buying a free JamTrack @@ -246,13 +226,13 @@ describe Sale do # we can see if the first one is ultimately destroyed it "rolls back created adjustments if error" do - shopping_cart = ShoppingCart.create user, jamtrack, 1, true + shopping_cart = ShoppingCart.create user, jamtrack, 1, false # grab the real response; we will modify it to make a nil accounting code adjustment_attrs = shopping_cart.create_adjustment_attributes(user) client.find_or_create_account(user, billing_info) - adjustment_attrs[1][:unit_amount_in_cents] = nil # invalid amount + adjustment_attrs[0][:unit_amount_in_cents] = nil # invalid amount ShoppingCart.any_instance.stub(:create_adjustment_attributes).and_return(adjustment_attrs) expect { Sale.place_order(user, [shopping_cart]) }.to raise_error(JamRuby::RecurlyClientError) @@ -265,7 +245,7 @@ describe Sale do end it "rolls back adjustments created before the order" do - shopping_cart = ShoppingCart.create user, jamtrack, 1, true + shopping_cart = ShoppingCart.create user, jamtrack, 1, false client.find_or_create_account(user, billing_info) # create a single adjustment on the account @@ -281,7 +261,7 @@ describe Sale do recurly_account = client.get_account(user) adjustments = recurly_account.adjustments - adjustments.should have(2).items # two adjustments are created for a free jamtrack; that should be all there is + adjustments.should have(1).items # two adjustments are created for a free jamtrack; that should be all there is end end diff --git a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb index 13f6777bc..6be02e8d4 100644 --- a/ruby/spec/jam_ruby/models/shopping_cart_spec.rb +++ b/ruby/spec/jam_ruby/models/shopping_cart_spec.rb @@ -21,34 +21,36 @@ describe ShoppingCart do user.shopping_carts[0].quantity.should == 1 end + + it "maintains only one fre JamTrack in ShoppingCart" do + cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart1.should_not be_nil + cart1.errors.any?.should be_false + user.reload + cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) + cart2.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(1) + cart3 = ShoppingCart.add_jam_track_to_cart(user, jam_track2) + cart3.errors.any?.should be_false + user.reload + user.shopping_carts.length.should eq(1) + end + it "should not add duplicate JamTrack to ShoppingCart" do + user.has_redeemable_jamtrack = false + user.save! cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart1.should_not be_nil cart1.errors.any?.should be_false user.reload cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track) cart2.errors.any?.should be_true - end describe "redeemable behavior" do - it "adds redeemable item to shopping cart" do - user.has_redeemable_jamtrack.should be_true - - # first item added to shopping cart should be marked for redemption - cart = ShoppingCart.add_jam_track_to_cart(user, jam_track) - cart.marked_for_redeem.should eq(1) - - # but the second item should not - - user.reload - - cart = ShoppingCart.add_jam_track_to_cart(user, jam_track2) - cart.marked_for_redeem.should eq(0) - end - - it "removes redeemable item to shopping cart" do + it "removes redeemable item to shopping cart (maintains only one in cart)" do user.has_redeemable_jamtrack.should be_true cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track) @@ -58,12 +60,12 @@ describe ShoppingCart do cart2.should_not be_nil cart1.marked_for_redeem.should eq(1) - cart2.marked_for_redeem.should eq(0) + cart2.marked_for_redeem.should eq(1) ShoppingCart.remove_jam_track_from_cart(user, jam_track) - user.shopping_carts.length.should eq(1) + user.shopping_carts.length.should eq(0) cart2.reload - cart1.marked_for_redeem.should eq(1) + cart2.marked_for_redeem.should eq(1) end end end diff --git a/ruby/spec/jam_ruby/models/signup_hint_spec.rb b/ruby/spec/jam_ruby/models/signup_hint_spec.rb index 1ec79efef..fc17d2e03 100644 --- a/ruby/spec/jam_ruby/models/signup_hint_spec.rb +++ b/ruby/spec/jam_ruby/models/signup_hint_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe SignupHint do - let(:user) {AnonymousUser.new(SecureRandom.uuid)} + let(:user) {AnonymousUser.new(SecureRandom.uuid, nil)} describe "refresh_by_anoymous_user" do it "creates" do diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 030fd6e88..08a2918e8 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -48,6 +48,9 @@ //= require utils //= require subscription_utils //= require custom_controls +//= require web/signup_helper +//= require web/signin_helper +//= require web/signin //= require_directory . //= require_directory ./dialog //= require_directory ./wizard diff --git a/web/app/assets/javascripts/checkout_payment.js b/web/app/assets/javascripts/checkout_payment.js index 900efd3ab..9b72f4fc5 100644 --- a/web/app/assets/javascripts/checkout_payment.js +++ b/web/app/assets/javascripts/checkout_payment.js @@ -560,20 +560,6 @@ $screen.find("#payment-info-next").on('click', next); } - function beforeShowOrder() { - step = 3; - renderNavigation(); - populateOrderPage(); - } - - - function populateOrderPage() { - - rest.getShoppingCarts() - .done(renderOrderPage) - .fail(app.ajaxError); - } - function toggleShippingAsBilling(e) { e.preventDefault(); diff --git a/web/app/assets/javascripts/checkout_signin.js b/web/app/assets/javascripts/checkout_signin.js index 6985c3b71..9dad91bdf 100644 --- a/web/app/assets/javascripts/checkout_signin.js +++ b/web/app/assets/javascripts/checkout_signin.js @@ -19,6 +19,7 @@ var $contentHolder = null; var $btnNext = null; var $btnFacebook = null; + var checkoutUtils = context.JK.CheckoutUtilsInstance; function beforeShow(data) { renderNavigation(); @@ -96,9 +97,23 @@ $signinBtn.text('TRYING...').addClass('disabled') rest.login({email: email, password: password, remember_me: true}) - .done(function() { - window.location = '/client#/checkoutPayment' - window.location.reload(); + .done(function(user) { + // now determine where we should send the user + rest.getShoppingCarts() + .done(function(carts) { + if(checkoutUtils.hasOneFreeItemInShoppingCart(carts)) { + window.location = '/client#/redeemComplete' + window.location.reload(); + } + else { + window.location = '/client#/checkoutPayment' + window.location.reload(); + } + }) + .fail(function() { + window.location = '/client#/jamtrackBrowse' + window.location.reload(); + }) }) .fail(function(jqXHR) { if(jqXHR.status == 422) { diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee index d97804ca5..7f61f0fb8 100644 --- a/web/app/assets/javascripts/checkout_utils.js.coffee +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -43,6 +43,16 @@ class CheckoutUtils getLastPurchase: () => return @lastPurchaseResponse + hasOneFreeItemInShoppingCart: (carts) => + + if carts.length == 0 + # nothing is in the user's shopping cart. They shouldn't be here. + return false; + else if carts.length > 1 + # the user has multiple items in their shopping cart. They shouldn't be here. + return false; + + return carts[0].product_info.free # global instance context.JK.CheckoutUtilsInstance = new CheckoutUtils() \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index b3df383e9..3dd131e41 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -89,6 +89,7 @@ rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) .done(function(response) { $dialog.data('result', {success:true, jamTrack: jamTrack}) + context.JK.CurrentSessionModel.updateSession(response);s app.layout.closeDialog('open-jam-track-dialog'); }) .fail(function(jqXHR) { diff --git a/web/app/assets/javascripts/dialog/signinDialog.js b/web/app/assets/javascripts/dialog/signinDialog.js index a76d649d5..a1b56b11d 100644 --- a/web/app/assets/javascripts/dialog/signinDialog.js +++ b/web/app/assets/javascripts/dialog/signinDialog.js @@ -11,11 +11,21 @@ var dialogId = '#signin-dialog'; var $dialog = null; var signinHelper = null; + var redirectTo = null; - function beforeShow() { + function beforeShow(args) { logger.debug("showing login form") - signinHelper.reset(); + if(args.redirect_to) { + redirectTo = "/client#/redeemComplete" + } + else { + redirectTo = null; + } + if(redirectTo) { + logger.debug("setting redirect to in login dialog") + } + signinHelper.reset(redirectTo); } function afterShow() { @@ -24,6 +34,7 @@ function afterHide() { logger.debug("hiding login form") + redirectTo = null; } function initialize(){ diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 800cdc276..58592a73c 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -206,6 +206,8 @@ context.JK.DownloadJamTrack = class DownloadJamTrack showInitial: () => @logger.debug("showing #{@state.name}") @sampleRate = context.jamClient.GetSampleRate() + @fingerprint = context.jamClient.SessionGetMacHash() + logger.debug("fingerprint: ", @fingerprint) @sampleRateForFilename = if @sampleRate == 48 then '48' else '44' @attempts = @attempts + 1 this.expectTransition() @@ -450,7 +452,7 @@ context.JK.DownloadJamTrack = class DownloadJamTrack @attemptedEnqueue = true @ajaxEnqueueAborted = false - @rest.enqueueJamTrack({id: @jamTrack.id, sample_rate: @sampleRate}) + @rest.enqueueJamTrack({id: @jamTrack.id, sample_rate: @sampleRate, fingerprint: @fingerprint}) .done(this.processEnqueueJamTrack) .fail(this.processEnqueueJamTrackFail) @@ -474,9 +476,24 @@ context.JK.DownloadJamTrack = class DownloadJamTrack else @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrack response") - processEnqueueJamTrackFail: () => + displayUIForGuard:(response) => + display = switch response.message + when 'no user specified' then 'Please log back in.' + when 'no fingerprint specified' then 'There was a problem communicating between client and server. Please restart JamKazam.' + when 'no all fingerprint specified' then 'There was a problem communicating between client and server. Please restart JamKazam.' + when 'no running fingerprint specified' then 'There was a problem communicating between client and server. Please restart JamKazam.' + when 'already redeemed another' then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com." + when "other user has 'all' fingerprint" then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com." + when "other user has 'running' fingerprint" then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com." + else "Something went wrong #{response.message}. Please restart JamKazam" + + processEnqueueJamTrackFail: (jqXHR) => unless @ajaxEnqueueAborted - this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.") + if jqXHR.status == 403 + display = this.displayUIForGuard(JSON.parse(jqXHR.responseText)) + this.transitionError("enqueue-error", display) + else + this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.") else @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrackFail response") diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index 741f70b3b..8186b3b58 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -210,6 +210,7 @@ if (!userProfile.show_jamtrack_guide && userProfile.show_whats_next && userProfile.show_whats_next_count < 10 && window.location.pathname.indexOf(gon.client_path) == 0 && window.location.hash.indexOf('/checkout') == -1 && + window.location.hash.indexOf('/redeem') == -1 && !app.layout.isDialogShowing('getting-started')) { app.layout.showDialog('getting-started'); diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 9b5115e2a..b8fcf54f6 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1662,6 +1662,26 @@ }); } + function signup(data) { + return $.ajax({ + type: "POST", + url: '/api/users', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data), + }); + } + + function portOverCarts() { + return $.ajax({ + type: "POST", + url: '/api/shopping_carts/port', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(data) + }) + } + function createAlert(subject, data) { var message = {subject:subject}; $.extend(message, data); @@ -1825,6 +1845,8 @@ this.playJamTrack = playJamTrack; this.createSignupHint = createSignupHint; this.createAlert = createAlert; + this.signup = signup; + this.portOverCarts = portOverCarts; return this; }; diff --git a/web/app/assets/javascripts/jam_track_preview.js.coffee b/web/app/assets/javascripts/jam_track_preview.js.coffee index 73cd2719e..58388278b 100644 --- a/web/app/assets/javascripts/jam_track_preview.js.coffee +++ b/web/app/assets/javascripts/jam_track_preview.js.coffee @@ -61,7 +61,7 @@ context.JK.JamTrackPreview = class JamTrackPreview if @options.master_shows_duration duration = 'entire song' if @jamTrack.duration - duration = "0:00 - #{context.JK.prettyPrintSeconds(@jamTrack.duration)}" + duration = "#{context.JK.prettyPrintSeconds(@jamTrack.duration)}" part = duration else part = @jamTrack.name + ' by ' + @jamTrack.original_artist diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee index 1dc48338e..64b970d19 100644 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -192,9 +192,23 @@ context.JK.JamTrackScreen=class JamTrackScreen addToCartJamtrack:(e) => e.preventDefault() - params = id: $(e.target).attr('data-jamtrack-id') + $target = $(e.target) + params = id: $target.attr('data-jamtrack-id') + isFree = $(e.target).is('.is_free') + rest.addJamtrackToShoppingCart(params).done((response) => - context.location = '/client#/shoppingCart' + if(isFree) + if context.JK.currentUserId? + alert("TODO") + context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices + context.location = '/client#/redeemComplete' + else + # now make a rest call to buy it + context.location = '/client#/redeemSignup' + + else + context.location = '/client#/shoppingCart' + ).fail @app.ajaxError licenseUSWhy:(e) => @@ -211,15 +225,15 @@ context.JK.JamTrackScreen=class JamTrackScreen if expand trackElement.find('.extra').removeClass('hidden') - detailArrow.html('hide tracks ') + detailArrow.html('hide tracks ') for track in jamTrack.tracks trackElement.find("[jamtrack-track-id='#{track.id}']").removeClass('hidden') else trackElement.find('.extra').addClass('hidden') - detailArrow.html('preview all tracks ') + detailArrow.html('show all tracks ') count = 0 for track in jamTrack.tracks - if count < 2 + if count < 6 trackElement.find("[jamtrack-track-id='#{track.id}']").removeClass('hidden') else trackElement.find("[jamtrack-track-id='#{track.id}']").addClass('hidden') @@ -281,14 +295,15 @@ context.JK.JamTrackScreen=class JamTrackScreen if track.part != '' track.instrument_desc += ' (' + track.part + ')' - free_state = if gon.global.one_free_jamtrack_per_user then 'free' else 'non-free' - if @user - free_state = if @user.free_jamtrack then 'free' else 'non-free' + free_state = if context.JK.currentUserFreeJamTrack then 'free' else 'non-free' + + is_free = free_state == 'free' options = jamtrack: trackRow expanded: false - free_state: free_state + free_state: free_state, + is_free: is_free @jamtrackItem = $(context._.template($('#template-jamtrack').html(), options, variable: 'data')) that.renderJamtrack(@jamtrackItem, jamtrack) that.registerEvents(@jamtrackItem) diff --git a/web/app/assets/javascripts/redeem_complete.js b/web/app/assets/javascripts/redeem_complete.js new file mode 100644 index 000000000..5f93cd4ce --- /dev/null +++ b/web/app/assets/javascripts/redeem_complete.js @@ -0,0 +1,264 @@ +(function (context, $) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RedeemCompleteScreen = function (app) { + + var EVENTS = context.JK.EVENTS; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var jamTrackUtils = context.JK.JamTrackUtils; + var checkoutUtils = context.JK.CheckoutUtilsInstance; + + var $screen = null; + var $navigation = null; + var $templatePurchasedJamTrack = null; + var $thanksPanel = null; + var $jamTrackInBrowser = null; + var $jamTrackInClient = null; + var $purchasedJamTrack = null; + var $purchasedJamTrackHeader = null; + var $purchasedJamTracks = null; + var userDetail = null; + var step = null; + var downloadJamTracks = []; + var purchasedJamTracks = null; + var purchasedJamTrackIterator = 0; + var $backBtn = null; + var $downloadApplicationLink = null; + var $noPurchasesPrompt = null; + var shoppingCartItem = null; + + + function beforeShow() { + + } + + function afterShow(data) { + $noPurchasesPrompt.addClass('hidden') + $purchasedJamTracks.empty() + $thanksPanel.addClass("hidden") + $purchasedJamTrackHeader.attr('status', 'in-progress') + $jamTrackInBrowser.addClass('hidden') + $jamTrackInClient.addClass('hidden') + + + // if there is no current user, but it apperas we have a login cookie, just refresh + if(!context.JK.currentUserId && $.cookie('remember_token')) { + window.location.reload(); + } + else { + redeemJamTrack() + } + + //prepThanks() + } + + function handleShoppingCartResponse(carts) { + + if(!checkoutUtils.hasOneFreeItemInShoppingCart(carts)) { + // the user has multiple items in their shopping cart. They shouldn't be here. + logger.error("invalid access of redeemComplete page") + window.location = '/client#/jamtrackBrowse' + } + else { + // ok, we have one, free item. save it for + shoppingCartItem = carts[0]; + + rest.placeOrder() + .done(function(purchaseResponse) { + context.JK.currentUserFreeJamTrack = false // make sure the user sees no more free notices without having to do a full page refresh + + checkoutUtils.setLastPurchase(purchaseResponse) + jamTrackUtils.checkShoppingCart() + //app.refreshUser() // this only causes grief in tests for some reason, and with currentUserFreeJamTrack = false above, this is probably now unnecessary + + prepThanks(); + }) + .fail(function() { + + }) + } + } + + function redeemJamTrack() { + rest.getShoppingCarts() + .done(handleShoppingCartResponse) + .fail(app.ajaxError); + + } + + function beforeHide() { + if(downloadJamTracks) { + context._.each(downloadJamTracks, function(downloadJamTrack) { + downloadJamTrack.destroy(); + downloadJamTrack.root.remove(); + }) + + downloadJamTracks = []; + } + purchasedJamTracks = null; + purchasedJamTrackIterator = 0; + } + + function prepThanks() { + showThanks(); + } + + function showThanks(purchaseResponse) { + + + var purchaseResponse = checkoutUtils.getLastPurchase(); + + if(!purchaseResponse || purchaseResponse.length == 0) { + // user got to this page with no context + logger.debug("no purchases found; nothing to show") + $noPurchasesPrompt.removeClass('hidden') + } + else { + if(gon.isNativeClient) { + $jamTrackInClient.removeClass('hidden') + } + else { + $jamTrackInBrowser.removeClass('hidden'); + } + $thanksPanel.removeClass('hidden') + handleJamTracksPurchased(purchaseResponse.jam_tracks) + } + } + + function handleJamTracksPurchased(jamTracks) { + // were any JamTracks purchased? + var jamTracksPurchased = jamTracks && jamTracks.length > 0; + if(jamTracksPurchased) { + if(gon.isNativeClient) { + $jamTrackInClient.removeClass('hidden') + context.JK.GA.virtualPageView('/redeemInClient'); + startDownloadJamTracks(jamTracks) + } + else { + $jamTrackInBrowser.removeClass('hidden'); + + app.user().done(function(user) { + // cut off time + var cutoff = new Date("May 8, 2015 00:00:00"); + if(new Date(user.created_at).getTime() < cutoff.getTime()) { + logger.debug("existing user recorded") + context.JK.GA.virtualPageView('/redeemInBrowserExistingUser'); + } + else { + logger.debug("new user recorded") + context.JK.GA.virtualPageView('/redeemInBrowserNewUser'); + } + }) + + + + app.user().done(function(user) { + if(!user.first_downloaded_client_at) { + $downloadApplicationLink.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.attr('status', 'done') + } + } + + function events() { + $backBtn.on('click', function(e) { + e.preventDefault(); + + context.location = '/client#/jamtrackBrowse' + }) + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow, + 'beforeHide': beforeHide + }; + app.bindScreen('redeemComplete', screenBindings); + + $screen = $("#redeemCompleteScreen"); + $templatePurchasedJamTrack = $('#template-purchased-jam-track'); + $thanksPanel = $screen.find(".thanks-panel"); + $jamTrackInBrowser = $screen.find(".jam-tracks-in-browser"); + $jamTrackInClient = $screen.find(".jam-tracks-in-client"); + $purchasedJamTrack = $thanksPanel.find(".thanks-detail.purchased-jam-track"); + $purchasedJamTrackHeader = $purchasedJamTrack.find(".purchased-jam-track-header"); + $purchasedJamTracks = $purchasedJamTrack.find(".purchased-list") + $backBtn = $screen.find('.back'); + $downloadApplicationLink = $screen.find('.download-jamkazam-wrapper'); + $noPurchasesPrompt = $screen.find('.no-purchases-prompt') + + if ($screen.length == 0) throw "$screen must be specified"; + + events(); + } + + this.initialize = initialize; + + return this; + } +}) +(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/redeem_signup.js b/web/app/assets/javascripts/redeem_signup.js new file mode 100644 index 000000000..bc7a13641 --- /dev/null +++ b/web/app/assets/javascripts/redeem_signup.js @@ -0,0 +1,244 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RedeemSignUpScreen = function(app) { + + var logger = context.JK.logger; + var rest = context.JK.Rest(); + + var $screen = null; + var $signupForm = null; + var $self = $(this); + var $email = null; + var $password = null; + var $firstName = null; + var $lastName = null; + + var $signupBtn = null; + var $inputElements = null; + var $contentHolder = null; + var $btnNext = null; + var $btnFacebook = null; + var $termsOfServiceL = null; + var $termsOfServiceR = null; + var shoppingCartItem = null; + var $jamtrackName = null; + var $signinLink = null; + + function beforeShow(data) { + renderLoggedInState(); + } + + function afterShow(data) { + } + + + function renderLoggedInState(){ + + if(isLoggedIn()) { + $contentHolder.removeClass('not-signed-in').addClass('signed-in') + } + else { + $jamtrackName.text('') + $contentHolder.addClass('hidden') + $contentHolder.removeClass('signed-in').addClass('not-signed-in') + // now check that the user has one, and only one, free jamtrack in their shopping cart. + rest.getShoppingCarts() + .done(handleShoppingCartResponse) + .fail(app.ajaxError); + } + } + + function isLoggedIn() { + return !!context.JK.currentUserId; + } + + function events() { + $btnFacebook.on('click', onClickSignupFacebook) + $signupForm.on('submit', signup) + $signupBtn.on('click', signup) + $signinLink.on('click', onSignin); + } + + function handleShoppingCartResponse(carts) { + + if(carts.length == 0) { + // nothing is in the user's shopping cart. They shouldn't be here. + logger.error("invalid access of redeemJamTrack page") + window.location = '/client#/jamtrackBrowse' + } + else if(carts.length > 1) { + // the user has multiple items in their shopping cart. They shouldn't be here. + logger.error("invalid access of redeemJamTrack page") + window.location = '/client#/jamtrackBrowse' + } + else { + var item = carts[0]; + + if(item.product_info.free) { + // ok, we have one, free item. save it for + shoppingCartItem = item; + $jamtrackName.text('"' + shoppingCartItem.product_info.name + '"') + $contentHolder.removeClass('hidden') + } + else { + // the user has a non-free, single item in their basket. They shouldn't be here. + logger.error("invalid access of redeemJamTrack page") + window.location = '/client#/jamtrackBrowse' + } + } + + var $latestCartHtml = ""; + + var any_in_us = false + context._.each(carts, function(cart) { + if(cart.product_info.sales_region == 'United States') { + any_in_us = true + } + }) + } + function onClickSignupFacebook() { + // tos must already be clicked + + var $btn = $(e.target) + $btn.addClass('disabled') + + var $field = $termsOfServiceL.closest('.field') + $field.find('.error-text').remove() + + logger.debug("field, ", $field, $termsOfServiceL) + if($termsOfServiceL.is(":checked")) { + + rest.createSignupHint({redirect_location: '/client#/redeemComplete'}) + .done(function() { + // send the user on to facebook signin + window.location = $btn.attr('href'); + }) + .fail(function() { + app.notify({text:"Facebook Signup is not working properly"}); + }) + .always(function() { + $btn.removeClass('disabled') + }) + } + else { + $field.addClass("error").addClass("transparent"); + $field.append("