VRFS-3936 merging develop

This commit is contained in:
Jonathan Kolyer 2017-02-24 14:05:57 -08:00
commit 81979ba4bb
145 changed files with 3503 additions and 1102 deletions

View File

@ -36,11 +36,11 @@ gem 'coffee-script-source' #, '~> 1.4.0' # ADD THIS LINE, 1.5.0 doesn't compile
gem 'devise' #, '3.3.0' gem 'devise' #, '3.3.0'
gem 'will_paginate' #, '3.0.3' gem 'will_paginate' #, '3.0.3'
gem 'bootstrap-will_paginate', '0.0.6' gem 'bootstrap-will_paginate', '0.0.6'
gem 'carrierwave' #, '0.9.0' gem 'carrierwave', '0.11.2' #, '0.9.0'
gem 'carrierwave_direct' gem 'carrierwave_direct'
gem 'uuidtools', '2.1.2' gem 'uuidtools', '2.1.2'
gem 'jquery-ui-rails' #, '4.2.1' gem 'jquery-ui-rails', '5.0.5' #, '4.2.1'
gem 'jquery-rails' gem 'jquery-rails', '4.1.1' # both this and jquery-ui-rails are pinned; if you unpin, jquery/autocomplete is missing during precomplie
gem 'rails-jquery-autocomplete' # This is the maintained version of rails3-jquery-autocomplete gem 'rails-jquery-autocomplete' # This is the maintained version of rails3-jquery-autocomplete
gem 'activeadmin' , '1.0.0.pre4'# github: 'activeadmin', branch: 'master' gem 'activeadmin' , '1.0.0.pre4'# github: 'activeadmin', branch: 'master'
gem 'mime-types', '1.25' gem 'mime-types', '1.25'

View File

@ -1,8 +1,12 @@
ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do
config.filters = false
menu :label => 'JamBlasters', :parent => 'JamBlaster' menu :label => 'JamBlasters', :parent => 'JamBlaster'
scope("Connected", default: true) { |scope| scope.where('client_id in (select client_id from connections)') }
scope("All") { |scope| scope.order('created_at desc') }
form do |f| form do |f|
f.inputs 'New JamBlaster' do f.inputs 'New JamBlaster' do
f.input :user, required: true, collection: User.all, include_blank: false f.input :user, required: true, collection: User.all, include_blank: false
@ -12,4 +16,25 @@ ActiveAdmin.register JamRuby::Jamblaster, :as => 'Jamblaster' do
end end
f.actions f.actions
end end
index do
# actions # use this for all view/edit/delete links
column 'Serial' do |oo|
oo.serial_no
end
column 'IPv4' do |oo|
oo.ipv4_link_local
end
column 'IPv6' do |oo|
oo.ipv6_link_local
end
end
end end

View File

@ -30,11 +30,17 @@ ActiveAdmin.register JamRuby::SaleLineItem, :as => 'Sale Line Items' do
end end
end end
column 'Source' do |oo| column 'Source' do |oo|
oo.sale.source if oo.sale.source == JamRuby::Sale::SOURCE_PAYPAL
link_to(oo.sale.source, 'https://history.paypal.com/webscr?cmd=_history-details-from-hub&id=' + oo.sale.recurly_invoice_id)
else
oo.sale.source
end
end end
column 'When' do |oo| column 'When' do |oo|
oo.created_at oo.created_at
end end
column 'Link' do |oo|
end
end end

View File

@ -368,4 +368,8 @@ retailers.sql
second_ed.sql second_ed.sql
second_ed_v2.sql second_ed_v2.sql
retailers_v2.sql retailers_v2.sql
retailer_interest.sql retailer_interest.sql
connection_role.sql
retailer_payment_split.sql
teacher_distribution_fields.sql
jam_track_download_rights.sql

View File

@ -0,0 +1,2 @@
ALTER TABLE connections ADD COLUMN client_role VARCHAR;
ALTER TABLE connections ADD COLUMN parent_client_id VARCHAR;

View File

@ -0,0 +1,8 @@
ALTER TABLE jam_tracks ADD COLUMN download_price numeric;
UPDATE jam_tracks SET download_price = 4.99;
ALTER TABLE jam_track_rights ADD COLUMN can_download BOOLEAN NOT NULL DEFAULT FALSE;
UPDATE jam_track_rights SET can_download = TRUE;
ALTER TABLE shopping_carts ADD COLUMN variant VARCHAR;
UPDATE shopping_carts set variant = 'stream' where cart_type = 'JamTrack';
ALTER TABLE sale_line_items ADD COLUMN variant VARCHAR;
UPDATE sale_line_items set variant = 'full';

View File

@ -0,0 +1,2 @@
ALTER TABLE retailers ADD COLUMN payment VARCHAR;
ALTER TABLE lesson_bookings ADD COLUMN payment VARCHAR;

View File

@ -0,0 +1,3 @@
ALTER TABLE teacher_distributions ADD COLUMN teacher_fee_in_cents INTEGER;
ALTER TABLE lesson_bookings ADD COLUMN same_retailer BOOLEAN DEFAULT FALSE NOT NULL;
ALTER TABLE lesson_sessions ADD COLUMN admin_marked BOOLEAN DEFAULT FALSE NOT NULL;

View File

@ -64,6 +64,7 @@ gem 'icalendar'
gem 'email_validator' gem 'email_validator'
group :test do group :test do
gem 'pry'
gem 'simplecov', '~> 0.7.1' gem 'simplecov', '~> 0.7.1'
gem 'simplecov-rcov' gem 'simplecov-rcov'
gem 'factory_girl', '4.5.0' gem 'factory_girl', '4.5.0'

View File

@ -40,6 +40,7 @@ require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error" require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/errors/jam_record_not_found" require "jam_ruby/errors/jam_record_not_found"
require "jam_ruby/errors/conflict_error" require "jam_ruby/errors/conflict_error"
require "jam_ruby/errors/pay_pal_client_error"
require "jam_ruby/lib/app_config" require "jam_ruby/lib/app_config"
require "jam_ruby/lib/s3_manager_mixin" require "jam_ruby/lib/s3_manager_mixin"
require "jam_ruby/lib/s3_public_manager_mixin" require "jam_ruby/lib/s3_public_manager_mixin"

View File

@ -42,7 +42,7 @@
yourself playing along with the rest of the band in audio or video, and more. Get your first yourself playing along with the rest of the band in audio or video, and more. Get your first
JamTrack free to try one out! After that they are just $1.99 each. <a href="https://jamkazam.desk.com/customer/en/portal/articles/2414618-playing-with-jamtracks" style="color:#fc0">Click here for more JamTrack free to try one out! After that they are just $1.99/$4.99 each. <a href="https://jamkazam.desk.com/customer/en/portal/articles/2414618-playing-with-jamtracks" style="color:#fc0">Click here for more
information</a> on how you can use JamTracks in your browser, in our free Mac or Windows information</a> on how you can use JamTracks in your browser, in our free Mac or Windows

View File

@ -402,7 +402,7 @@ SQL
music_session.creator music_session.creator
end end
def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil) def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, client_role = nil, parent_client_id = nil, video_sources=nil)
connection = nil connection = nil
ConnectionManager.active_record_transaction do |connection_manager| ConnectionManager.active_record_transaction do |connection_manager|
@ -418,7 +418,7 @@ SQL
raise JamPermissionError, "wrong user_id associated with connection #{client_id}" raise JamPermissionError, "wrong user_id associated with connection #{client_id}"
end end
connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, video_sources) connection.join_the_session(music_session, as_musician, tracks, user, audio_latency, client_role, parent_client_id, video_sources)
JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id, client_id) JamRuby::MusicSessionUserHistory.join_music_session(user.id, music_session.id, client_id)
# connection.music_session_id = music_session.id # connection.music_session_id = music_session.id
# connection.as_musician = as_musician # connection.as_musician = as_musician

View File

@ -0,0 +1,19 @@
module JamRuby
class PayPalClientError < StandardError
attr_accessor :errors
def initialize(data)
if data.respond_to?('has_key?')
self.errors = data
else
self.errors = {:message=>data.to_s}
end
end # initialize
def to_s
s=super
s << ", errors: #{errors.inspect}" if self.errors.any?
s
end
end
end

View File

@ -868,6 +868,7 @@ module JamRuby
jam_track.genres = determine_genres(metadata) jam_track.genres = determine_genres(metadata)
jam_track.language = determine_language(metadata) jam_track.language = determine_language(metadata)
jam_track.price = 1.99 jam_track.price = 1.99
jam_track.download_price = 4.99
jam_track.reproduction_royalty_amount = nil jam_track.reproduction_royalty_amount = nil
jam_track.reproduction_royalty = true jam_track.reproduction_royalty = true
jam_track.public_performance_royalty = true jam_track.public_performance_royalty = true

View File

@ -501,7 +501,7 @@ module JamRuby
[music_sessions, user_scores] [music_sessions, user_scores]
end end
def self.participant_create(user, music_session_id, client_id, as_musician, tracks, audio_latency, video_sources=nil) def self.participant_create(user, music_session_id, client_id, as_musician, tracks, audio_latency, client_role = nil, parent_client_id = nil, video_sources=nil)
music_session = MusicSession.find(music_session_id) music_session = MusicSession.find(music_session_id)
# USERS ARE ALREADY IN SESSION # USERS ARE ALREADY IN SESSION
@ -514,7 +514,7 @@ module JamRuby
active_music_session.with_lock do # VRFS-1297 active_music_session.with_lock do # VRFS-1297
active_music_session.tick_track_changes active_music_session.tick_track_changes
# VRFS-3986 # VRFS-3986
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, client_role, parent_client_id, video_sources)
if connection.errors.any? if connection.errors.any?
# rollback the transaction to make sure nothing is disturbed in the database # rollback the transaction to make sure nothing is disturbed in the database
@ -573,7 +573,7 @@ module JamRuby
# auto-join this user into the newly created session # auto-join this user into the newly created session
as_musician = true as_musician = true
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, video_sources) connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency, client_role, parent_client_id, video_sources)
unless connection.errors.any? unless connection.errors.any?
user.update_progression_field(:first_music_session_at) user.update_progression_field(:first_music_session_at)

View File

@ -176,11 +176,13 @@ module JamRuby
true true
end end
def join_the_session(music_session, as_musician, tracks, user, audio_latency, videos=nil) def join_the_session(music_session, as_musician, tracks, user, audio_latency, client_role = nil, parent_client_id = nil, videos=nil)
self.music_session_id = music_session.id self.music_session_id = music_session.id
self.as_musician = as_musician == true # this is deliberate; otherwise we create a warning in one our tests that passes 'blarg' (rails warning about casting strings to false) self.as_musician = as_musician == true # this is deliberate; otherwise we create a warning in one our tests that passes 'blarg' (rails warning about casting strings to false)
self.joining_session = true self.joining_session = true
self.joined_session_at = Time.now self.joined_session_at = Time.now
self.client_role = client_role
self.parent_client_id = parent_client_id
associate_tracks(tracks) unless tracks.nil? associate_tracks(tracks) unless tracks.nil?
associate_videos(videos) unless videos.nil? associate_videos(videos) unless videos.nil?
self.save self.save

View File

@ -28,6 +28,10 @@ module JamRuby
sale_display sale_display
end end
def variant_price(variant = nil)
price
end
def price def price
if card_type == JAM_TRACKS_5 if card_type == JAM_TRACKS_5
10.00 10.00
@ -39,7 +43,7 @@ module JamRuby
end end
def sale_display def sale_display(variant = nil)
if card_type == JAM_TRACKS_5 if card_type == JAM_TRACKS_5
'JamTracks Gift Card (5)' 'JamTracks Gift Card (5)'
elsif card_type == JAM_TRACKS_10 elsif card_type == JAM_TRACKS_10

View File

@ -161,9 +161,22 @@ module JamRuby
true true
end end
def sale_display def sale_display(variant = nil)
"JamTrack: " + name if variant == ShoppingCart::JAMTRACK_FULL
variant_desc = 'FULL'
elsif variant == ShoppingCart::JAMTRACK_DOWNLOAD
variant_desc = 'UPRGADE'
elsif variant == ShoppingCart::JAMTRACK_STREAM
variant_desc = 'FOR USE ONLY WITHIN APP'
else
variant_desc = 'UNKNOWN'
end
"JamTrack: #{name} - #{variant_desc}"
end end
def duplicate_positions? def duplicate_positions?
counter = {} counter = {}
jam_track_tracks.each do |track| jam_track_tracks.each do |track|
@ -504,14 +517,43 @@ module JamRuby
owners.include?(user) owners.include?(user)
end end
def right_for_user(user) def right_for_user(user, variant = nil)
jam_track_rights.where("user_id=?", user).first
query = jam_track_rights.where("user_id=?", user)
if variant
if variant == ShoppingCart::JAMTRACK_DOWNLOAD
query = query.where('can_download', true)
elsif variant == ShoppingCart::JAMTRACK_FULL
query = query.where('can_download', true)
elsif variant == ShoppingCart::JAMTRACK_STREAM
else
throw 'unknown variant ' + variant
end
end
query.first
end end
def mixdowns_for_user(user) def mixdowns_for_user(user)
JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id) JamTrackMixdown.where(user_id: user.id).where(jam_track_id: self.id)
end end
def upgrade_price
variant_price('download')
end
def variant_price(variant)
if variant == 'full'
download_price
elsif variant == 'download'
download_price - price
else
price
end
end
def short_plan_code def short_plan_code
prefix = 'jamtrack-' prefix = 'jamtrack-'
plan_code[prefix.length..-1] plan_code[prefix.length..-1]

View File

@ -45,6 +45,7 @@ module JamRuby
belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking, :dependent => :destroy belongs_to :default_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :default_slot_id, inverse_of: :defaulted_booking, :dependent => :destroy
belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy belongs_to :counter_slot, class_name: "JamRuby::LessonBookingSlot", foreign_key: :counter_slot_id, inverse_of: :countered_booking, :dependent => :destroy
belongs_to :school, class_name: "JamRuby::School" belongs_to :school, class_name: "JamRuby::School"
belongs_to :retailer, class_name: "JamRuby::Retailer"
belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice" belongs_to :test_drive_package_choice, class_name: "JamRuby::TestDrivePackageChoice"
belongs_to :posa_card, class_name: "JamRuby::PosaCard" belongs_to :posa_card, class_name: "JamRuby::PosaCard"
has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy has_many :lesson_booking_slots, class_name: "JamRuby::LessonBookingSlot", :dependent => :destroy
@ -527,11 +528,15 @@ module JamRuby
end end
end end
def distribution_price_in_cents(target, education) def distribution_price_in_cents(target, education, split = nil, fee_rate = nil)
distribution = teacher_distribution_price_in_cents(target) distribution = teacher_distribution_price_in_cents(target)
if education if split
(distribution * 0.0625).round (distribution * split).round
# when a split is provided, we also pin down the teacher_fee_in_cents, instead of assuming a bunch of stuff
elsif education
(distribution * 0.0625).round # 0.0625 is 1/4th of 25%
else else
distribution distribution
end end
@ -799,6 +804,13 @@ module JamRuby
lesson_booking.school = lesson_booking.teacher.teacher.school lesson_booking.school = lesson_booking.teacher.teacher.school
end end
# copy payment settings from retailer into lesson booking
if lesson_booking.teacher && lesson_booking.teacher.teacher.retailer
lesson_booking.retailer = lesson_booking.teacher.teacher.retailer
lesson_booking.payment = lesson_booking.teacher.teacher.retailer.payment_details.to_json
lesson_booking.same_retailer = lesson_booking.teacher.teacher.retailer.affiliate_partner == user.affiliate_referral
end
if user if user
lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id)) lesson_booking.same_school = !!(lesson_booking.school && user.school && (lesson_booking.school.id == user.school.id))
if lesson_booking.same_school if lesson_booking.same_school

View File

@ -53,11 +53,11 @@ module JamRuby
end end
def teacher_distribution def teacher_distribution
teacher_distributions.where(education:false).first teacher_distributions.where(education: false).first
end end
def education_distribution def education_distribution
teacher_distributions.where(education:true).first teacher_distributions.where(education: true).first
end end
def add_test_drives def add_test_drives
@ -97,15 +97,46 @@ module JamRuby
purchase.month = month purchase.month = month
purchase.recurring = true purchase.recurring = true
# this is for monthly # this is for monthly
if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase) if lesson_booking && lesson_booking.requires_teacher_distribution?(purchase)
teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false)
purchase.teacher_distributions << teacher_dist
# price should always match the teacher_distribution, if there is one
purchase.price = teacher_dist.amount_in_cents / 100
if lesson_booking.school_on_school_payment? if lesson_booking.posa_card.nil? && lesson_booking.payment
purchase.teacher_distributions << TeacherDistribution.create_for_lesson_package_purchase(purchase, true)
# if there is a payment object, it will describe how everything gets doled out
payment = JSON.parse(lesson_booking.payment)
teacher_split = payment["teacher"]
retailer_split = payment["retailer"]
retailer_rate = teacher.teacher.retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee] # add 0.03 to account for stripe deduction
if teacher_split && teacher_split > 0
teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false, (teacher_split / 100.0).round(2), retailer_rate)
purchase.teacher_distributions << teacher_dist
# price should always match the teacher_distribution, if there is one
purchase.price = teacher_dist.amount_in_cents / 100
end
if retailer_split && retailer_split > 0
teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false, (retailer_split / 100.0).round(2), retailer_rate)
teacher_dist.retailer = teacher.teacher.retailer
teacher_dist.save
purchase.teacher_distributions << teacher_dist
end
else
teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, false)
purchase.teacher_distributions << teacher_dist
# price should always match the teacher_distribution, if there is one
purchase.price = teacher_dist.amount_in_cents / 100
if lesson_booking.school_on_school_payment?
teacher_dist = TeacherDistribution.create_for_lesson_package_purchase(purchase, true)
purchase.teacher_distributions << teacher_dist
end
end end
end end
else else

View File

@ -102,10 +102,13 @@ module JamRuby
end end
def sale_display def sale_display(variant = nil)
name name
end end
def variant_price(variant = nil)
price
end
def plan_code def plan_code
if package_type == SINGLE_FREE if package_type == SINGLE_FREE
"lesson-package-single-free" "lesson-package-single-free"

View File

@ -97,13 +97,17 @@ module JamRuby
end end
def teacher_distribution def teacher_distribution
teacher_distributions.where(education:false).first teacher_distributions.where(education:false).where('retailer_id is null').first
end end
def education_distribution def education_distribution
teacher_distributions.where(education:true).first teacher_distributions.where(education:true).first
end end
def retailer_distribution
teacher_distributions.where('retailer_id is not null').first
end
def manage_slot_changes def manage_slot_changes
# if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted . # if this slot changed, we need to update the time. But LessonBooking does this for us, for requested/accepted .
# TODO: what to do, what to do. # TODO: what to do, what to do.
@ -197,6 +201,59 @@ module JamRuby
counterer_id.nil? || counterer_id == student_id counterer_id.nil? || counterer_id == student_id
end end
def mark_lesson(success, administratively_marked = false)
self.success = success
self.analysed_at = Time.now
self.analysed = true
self.status = STATUS_COMPLETED
self.admin_marked = administratively_marked
if success && lesson_booking.requires_teacher_distribution?(self)
if lesson_booking.posa_card.nil? && lesson_booking.payment
# if there is a payment object, it will describe how everything gets doled out
payment = JSON.parse(lesson_booking.payment)
teacher_split = payment["teacher"]
retailer_split = payment["retailer"]
retailer_rate = teacher.teacher.retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee] # add 0.03 to account for stripe deduction
# but we must also take the correct rate out of the
if is_test_drive?
# test drives do not get subject to split behavior, per quick convo with David
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false)
else
if teacher_split && teacher_split > 0
teacher_dist = TeacherDistribution.create_for_lesson(self, false, (teacher_split / 100.0).round(2), retailer_rate)
self.teacher_distributions << teacher_dist
end
if retailer_split && retailer_split > 0
teacher_dist = TeacherDistribution.create_for_lesson(self, false, (retailer_split / 100.0).round(2), retailer_rate)
teacher_dist.retailer = teacher.teacher.retailer
teacher_dist.save
end
end
else
is_education_school_on_school = lesson_booking.school_on_school_payment?
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false)
if is_education_school_on_school
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true)
end
end
end
if self.save
# send out emails appropriate for this type of session
session_completed
end
end
def analyse def analyse
if self.analysed if self.analysed
return return
@ -210,23 +267,8 @@ module JamRuby
# extra protection against bad code somewhere # extra protection against bad code somewhere
return return
end end
self.success = analysis[:bill]
self.analysed_at = Time.now
self.analysed = true
self.status = STATUS_COMPLETED mark_lesson(analysis[:bill])
if success && lesson_booking.requires_teacher_distribution?(self)
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false)
if lesson_booking.school_on_school_payment?
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true)
end
end
if self.save
# send out emails appropriate for this type of session
session_completed
end
end end
def billed def billed

View File

@ -948,6 +948,9 @@ SQL
if scheduled_duration.class == String if scheduled_duration.class == String
duration = scheduled_duration.to_i.seconds duration = scheduled_duration.to_i.seconds
end end
if duration == 0
duration = 30 * 60
end
duration duration
end end
@ -996,7 +999,7 @@ SQL
"#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}"
end end
else else
"#{start_time.strftime("%A, %B %e")} - #{start_time.strftime("%l:%M%P").strip}" "#{start_time.strftime("%A, %B %e")} - #{end_time.strftime("%l:%M%P").strip}"
end end
else else

View File

@ -36,6 +36,10 @@ module JamRuby
sale_display sale_display
end end
def variant_price(variant = nil)
price
end
def price def price
if card_type == JAM_TRACKS_5 if card_type == JAM_TRACKS_5
10.00 10.00
@ -49,7 +53,7 @@ module JamRuby
end end
def sale_display def sale_display(variant = nil)
if card_type == JAM_TRACKS_5 if card_type == JAM_TRACKS_5
'JamTracks Card (5)' 'JamTracks Card (5)'
elsif card_type == JAM_TRACKS_10 elsif card_type == JAM_TRACKS_10

View File

@ -272,7 +272,9 @@ module JamRuby
music_session.connections.each do |connection| music_session.connections.each do |connection|
connection.tracks.each do |track| connection.tracks.each do |track|
recording.recorded_tracks << RecordedTrack.create_from_track(track, recording) if connection.client_role != 'child'
recording.recorded_tracks << RecordedTrack.create_from_track(track, recording)
end
end end
connection.video_sources.each do |video| connection.video_sources.each do |video|

View File

@ -16,6 +16,7 @@ module JamRuby
has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution' has_many :teacher_distributions, class_name: 'JamRuby::TeacherDistribution'
has_many :sales, class_name: 'JamRuby::Sale' has_many :sales, class_name: 'JamRuby::Sale'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem' has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
has_many :lesson_bookings, class_name: 'JamRuby::LessonBooking'
validates :user, presence: true validates :user, presence: true
#validates :slug, presence: true #validates :slug, presence: true
@ -60,11 +61,23 @@ module JamRuby
BCrypt::Password.new(self.encrypted_password) == password BCrypt::Password.new(self.encrypted_password) == password
end end
def update_payment(split)
if split[:teacher] && split[:teacher].is_a?(Integer) && split[:retailer] && split[:retailer].is_a?(Integer)
teacher_split = split[:teacher]
retailer_split = split[:retailer]
if (teacher_split >= 0 && teacher_split <= 100) && (retailer_split >= 0 && retailer_split <= 100) && (teacher_split + retailer_split == 100)
self.payment = split.to_json
end
end
end
def update_from_params(params) def update_from_params(params)
self.name = params[:name] if params[:name].present? self.name = params[:name] if params[:name].present?
self.city = params[:city] self.city = params[:city]
self.state = params[:state] self.state = params[:state]
self.slug = params[:slug] if params[:slug].present? self.slug = params[:slug] if params[:slug].present?
if params[:split]
self.update_payment(params[:split])
end
if params[:password].present? if params[:password].present?
self.should_validate_password = true self.should_validate_password = true
@ -74,6 +87,15 @@ module JamRuby
self.save self.save
end end
# should be of form {teacher: 0-100, retailer: 0-100}
def payment_details
if self.payment
JSON.parse(self.payment)
else
{"teacher" =>100, "retailer" => 0}
end
end
def owner def owner
user user
end end

View File

@ -9,6 +9,7 @@ module JamRuby
SOURCE_RECURLY = 'recurly' SOURCE_RECURLY = 'recurly'
SOURCE_IOS = 'ios' SOURCE_IOS = 'ios'
SOURCE_PAYPAL = 'paypal'
belongs_to :retailer, class_name: 'JamRuby::Retailer' belongs_to :retailer, class_name: 'JamRuby::Retailer'
belongs_to :user, class_name: 'JamRuby::User' belongs_to :user, class_name: 'JamRuby::User'
@ -117,8 +118,24 @@ module JamRuby
price_info price_info
end end
def self.ios_purchase(current_user, jam_track, receipt, price_data) def self.ios_purchase(current_user, jam_track, receipt, price_data, variant)
jam_track_right = nil
if variant.nil?
variant = ShoppingCart::JAMTRACK_STREAM
end
# see if we should bail because we already own these rights
jam_track_right = jam_track.right_for_user(current_user)
if !jam_track_right.nil? && jam_track_right.can_download
# if the user already has full rights to the JamTrack, there is nothing else to do in this path
return jam_track_right
end
if !jam_track_right.nil? && (!jam_track_right.can_download && variant == ShoppingCart::JAMTRACK_STREAM)
# if the user does have the track, but isn't upgrading it, bail
return jam_track_right
end
# everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it # everything needs to go into a transaction! If anything goes wrong, we need to raise an exception to break it
Sale.transaction do Sale.transaction do
@ -157,6 +174,11 @@ module JamRuby
jam_track_right.redeemed = using_free_credit jam_track_right.redeemed = using_free_credit
jam_track_right.version = jam_track.version jam_track_right.version = jam_track.version
end end
if variant == ShoppingCart::JAMTRACK_DOWNLOAD || variant == ShoppingCart::JAMTRACK_FULL
jam_track_right.can_download = true
jam_track_right.save
end
end end
jam_track_right jam_track_right
@ -166,7 +188,7 @@ module JamRuby
# individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation) # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
# jamtracks however can be piled onto the same sale as adjustments (VRFS-3028) # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
# so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
def self.place_order(current_user, shopping_carts) def self.place_order(current_user, shopping_carts, paypal = false)
sales = [] sales = []
@ -176,7 +198,7 @@ module JamRuby
# return sales # return sales
#end #end
jam_track_sale = order_jam_tracks(current_user, shopping_carts) jam_track_sale = order_jam_tracks(current_user, shopping_carts, paypal)
sales << jam_track_sale if jam_track_sale sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions # TODO: process shopping_carts_subscriptions
@ -377,7 +399,7 @@ module JamRuby
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # 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 # 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) def self.order_jam_tracks(current_user, shopping_carts, is_paypal)
shopping_carts_jam_tracks = [] shopping_carts_jam_tracks = []
shopping_carts_subscriptions = [] shopping_carts_subscriptions = []
@ -395,11 +417,9 @@ module JamRuby
end end
end end
client = RecurlyClient.new
sale = nil sale = nil
Sale.transaction do Sale.transaction do
sale = create_jam_track_sale(current_user, SOURCE_RECURLY) sale = create_jam_track_sale(current_user, is_paypal ? SOURCE_PAYPAL : SOURCE_RECURLY)
if sale.valid? if sale.valid?
if is_only_freebie(shopping_carts) if is_only_freebie(shopping_carts)
@ -429,61 +449,142 @@ module JamRuby
else else
account = client.get_account(current_user) if is_paypal
if account.present?
purge_pending_adjustments(account) sale.process_shopping_carts(current_user, shopping_carts)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account) paypal_auth = current_user.paypal_auth
# now invoice the sale ... almost done @api = PayPal::SDK::Merchant::API.new
@get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
@response = @api.get_express_checkout_details(@get_express_checkout_details)
begin @@log.info("User #{current_user.email}, GetExpressCheckout: #{@response.inspect}")
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid tax = false
sale.recurly_invoice_number = invoice.invoice_number if @response.Ack == 'Success'
payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
# we need to ask for taxes
tax = true
end
end
tax_rate = tax ? 0.0825 : 0
total = current_user.shopping_cart_total.round(2)
tax_total = (total * tax_rate).round(2)
total = total + tax_total
total = total.round(2)
@do_express_checkout_payment = @api.build_do_express_checkout_payment({
:DoExpressCheckoutPaymentRequestDetails => {
:PaymentDetails =>
[
{
:OrderTotal => {
:currencyID => "USD",
:value => total
},
:PaymentAction => "Sale"
}
],
:Token => paypal_auth.token,
:PayerID => paypal_auth.uid, }})
@pay_response = @api.do_express_checkout_payment(@do_express_checkout_payment)
@@log.info("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
# #<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseType:0x007fe511dd9b88
# @Timestamp=Sun, 11 Dec 2016 02:09:31 +0000, @Ack="Success",
# @CorrelationID="b28faf6bd90d9", @Version="117.0", @Build="000000",
# @DoExpressCheckoutPaymentResponseDetails=#<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseDetailsType:0x007fe511dd38c8
# @Token="EC-7A4606566T700564B",
# @PaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::PaymentInfoType:0x007fe511dd3008 @TransactionID="63C410710F2619403", @ParentTransactionID=nil,
# @ReceiptID=nil, @TransactionType="express-checkout", @PaymentType="instant", @PaymentDate=Sun, 11 Dec 2016 02:09:31 +0000,
# @GrossAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd0c90 @currencyID="USD", @value="1.99">,
# @FeeAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd04e8 @currencyID="USD", @value="0.36">,
# @TaxAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dcbe98 @currencyID="USD", @value="0.00">,
# @ExchangeRate=nil, @PaymentStatus="Completed", @PendingReason="none", @ReasonCode="none", @ProtectionEligibility="Eligible",
# @ProtectionEligibilityType="ItemNotReceivedEligible,UnauthorizedPaymentEligible", @SellerDetails=#<PayPal::SDK::Merchant::DataTypes::SellerDetailsType:0x007fe511dcb358 @SecureMerchantAccountID="6MB486RSBRMJ2">>],
# @SuccessPageRedirectRequested="false", @CoupledPaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::CoupledPaymentInfoType:0x007fe511dca7a0>]>>
if @pay_response.Ack == 'Success'
details = @pay_response.DoExpressCheckoutPaymentResponseDetails.PaymentInfo[0]
sale.recurly_invoice_id = details.TransactionID
sale.recurly_invoice_number = details.ReceiptID
# now slap in all the real tax/purchase totals # now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents sale.recurly_subtotal_in_cents = ((details.GrossAmount.value.to_f - details.TaxAmount.value.to_f) * 100).to_i
sale.recurly_tax_in_cents = invoice.tax_in_cents sale.recurly_tax_in_cents = (details.TaxAmount.value.to_f * 100).to_i
sale.recurly_total_in_cents = invoice.total_in_cents sale.recurly_total_in_cents = (details.GrossAmount.value.to_f * 100).to_i
sale.recurly_currency = invoice.currency sale.recurly_currency = details.GrossAmount.currencyID
# 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
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
unless sale.save unless sale.save
puts "WTF" puts "Invalid sale (at end)."
raise RecurlyClientError, "Invalid sale (at end)." raise PayPalClientError, "Invalid sale (at end)."
end end
rescue Recurly::Resource::Invalid => e else
# this exception is thrown by invoice! if the invoice is invalid @@log.error("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
sale.rollback_adjustments(current_user, created_adjustments) raise PayPalClientError, @pay_response.Errors[0].LongMessage
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
rescue => e
puts "UNKNOWN E #{e}"
end end
else else
raise RecurlyClientError, "Could not find account to place order." client = RecurlyClient.new
account = client.get_account(current_user)
if account.present?
purge_pending_adjustments(account)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, 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
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
unless sale.save
puts "Invalid sale (at end)."
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
rescue => e
puts "UNKNOWN E #{e}"
end
else
raise RecurlyClientError, "Could not find account to place order."
end
end end
end end
else else
@ -493,7 +594,7 @@ module JamRuby
sale sale
end end
def process_shopping_carts(current_user, shopping_carts, account) def process_shopping_carts(current_user, shopping_carts, account = nil)
created_adjustments = [] created_adjustments = []
@ -515,7 +616,7 @@ module JamRuby
end end
def process_shopping_cart(current_user, shopping_cart, account, created_adjustments) def process_shopping_cart(current_user, shopping_cart, recurly_account, created_adjustments)
recurly_adjustment_uuid = nil recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil recurly_adjustment_credit_uuid = nil
@ -527,7 +628,8 @@ module JamRuby
if shopping_cart.is_jam_track? if shopping_cart.is_jam_track?
jam_track = cart_product jam_track = cart_product
if jam_track.right_for_user(current_user)
if jam_track.right_for_user(current_user, shopping_cart.variant)
# if the user already owns the JamTrack, we should just skip this cart item, and destroy it # if the user already owns the JamTrack, we should just skip this cart item, and destroy it
# if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop # if this occurs, we have to reload every shopping_cart as we iterate. so, we do at the top of the loop
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart) ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
@ -536,14 +638,14 @@ module JamRuby
end end
if account if recurly_account
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user) 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) # 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 = recurly_account.adjustments.new(adjustment)
created_adjustment.save created_adjustment.save
# if the adjustment could not be made, bail # if the adjustment could not be made, bail
@ -583,6 +685,13 @@ module JamRuby
jam_track_right.version = jam_track.version jam_track_right.version = jam_track.version
end end
# deal with variant behavior
if shopping_cart.purchasing_downloadable_rights?
jam_track_right.can_download = true
jam_track_right.save
end
# also if the purchase was a free one, then: # also if the purchase was a free one, then:
# first, mark the free has_redeemable_jamtrack field if that's still true # first, mark the free has_redeemable_jamtrack field if that's still true
# and if still they have more free things, then redeem the giftable_jamtracks # and if still they have more free things, then redeem the giftable_jamtracks

View File

@ -175,6 +175,7 @@ module JamRuby
sale_line_item = SaleLineItem.new sale_line_item = SaleLineItem.new
sale_line_item.product_type = shopping_cart.cart_type sale_line_item.product_type = shopping_cart.cart_type
sale_line_item.variant = shopping_cart.variant
sale_line_item.unit_price = product_info[:price] sale_line_item.unit_price = product_info[:price]
sale_line_item.quantity = product_info[:quantity] sale_line_item.quantity = product_info[:quantity]
sale_line_item.free = product_info[:marked_for_redeem] sale_line_item.free = product_info[:marked_for_redeem]

View File

@ -10,6 +10,11 @@ module JamRuby
PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT] PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT]
JAMTRACK_FULL = 'full'
JAMTRACK_STREAM = 'stream'
JAMTRACK_DOWNLOAD = 'download'
JAMTRACK_VARIANTS = ['full', 'stream', 'download']
attr_accessible :quantity, :cart_type, :product_info attr_accessible :quantity, :cart_type, :product_info
attr_accessor :skip_mix_check attr_accessor :skip_mix_check
@ -22,13 +27,15 @@ module JamRuby
validates :cart_type, presence: true validates :cart_type, presence: true
validates :cart_class_name, presence: true validates :cart_class_name, presence: true
validates :marked_for_redeem, numericality: {only_integer: true} validates :marked_for_redeem, numericality: {only_integer: true}
validates :variant, inclusion: {in: [nil, JAMTRACK_FULL, JAMTRACK_STREAM, JAMTRACK_DOWNLOAD]}
#validate :not_mixed #validate :not_mixed
default_scope { order('created_at DESC') } default_scope { order('created_at DESC') }
def product_info(instance = nil) def product_info(instance = nil)
product = self.cart_product product = self.cart_product
data = {type: cart_type, name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display, allow_free: allow_free(product)} unless product.nil? data = {type: cart_type, name: product.name, price: product.variant_price(variant), product_id: cart_id, plan_code: product.plan_code, real_price: real_price(product), total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem, free: free?, sales_region: product.sales_region, sale_display:product.sale_display(variant), allow_free: allow_free(product)} unless product.nil?
if data && instance if data && instance
data.merge!(instance.product_info) data.merge!(instance.product_info)
end end
@ -37,12 +44,16 @@ module JamRuby
# multiply quantity by price # multiply quantity by price
def total_price(product) def total_price(product)
quantity * product.price quantity * product.variant_price(variant)
end
def purchasing_downloadable_rights?
is_jam_track? && (variant == ShoppingCart::JAMTRACK_DOWNLOAD || variant == ShoppingCart::JAMTRACK_FULL)
end end
# multiply (quantity - redeemable) by price # multiply (quantity - redeemable) by price
def real_price(product) def real_price(product)
(quantity - marked_for_redeem) * product.price (quantity - marked_for_redeem) * product.variant_price(variant)
end end
def allow_free(product) def allow_free(product)
@ -101,7 +112,7 @@ module JamRuby
end end
end end
def self.create user, product, quantity = 1, mark_redeem = false def self.create(user, product, quantity = 1, mark_redeem = false, variant = nil)
cart = ShoppingCart.new cart = ShoppingCart.new
if user.is_a?(User) if user.is_a?(User)
@ -111,6 +122,14 @@ module JamRuby
end end
cart.cart_type = product.class::PRODUCT_TYPE cart.cart_type = product.class::PRODUCT_TYPE
if cart.cart_type == JamTrack::PRODUCT_TYPE && variant.nil?
cart.variant = JAMTRACK_STREAM # default to jamtrack 'stream'
else
cart.variant = variant
end
cart.cart_class_name = product.class.name cart.cart_class_name = product.class.name
cart.cart_id = product.id cart.cart_id = product.id
cart.quantity = quantity cart.quantity = quantity
@ -158,9 +177,9 @@ module JamRuby
shopping_carts.each do |shopping_cart| shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track? if shopping_cart.is_jam_track?
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user) mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(user)
cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem) cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, mark_redeem, shopping_cart.variant)
else else
cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false) cart = ShoppingCart.create(user, shopping_cart.cart_product, shopping_cart.quantity, false, shopping_cart.variant)
end end
end end
@ -201,8 +220,13 @@ module JamRuby
end end
# adds a jam_track to cart, checking for promotions # adds a jam_track to cart, checking for promotions
def self.add_jam_track_to_cart(any_user, jam_track, clear:false) def self.add_jam_track_to_cart(any_user, jam_track, variant = JAMTRACK_FULL)
cart = nil cart = nil
if variant.nil?
variant = JAMTRACK_FULL
end
ShoppingCart.transaction do ShoppingCart.transaction do
# if clear # if clear
@ -213,7 +237,7 @@ module JamRuby
end end
mark_redeem = jam_track.allow_free ? ShoppingCart.user_has_redeemable_jam_track?(any_user) : false mark_redeem = jam_track.allow_free ? ShoppingCart.user_has_redeemable_jam_track?(any_user) : false
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem) cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem, variant)
end end
any_user.reload any_user.reload
cart cart

View File

@ -43,26 +43,35 @@ module JamRuby
end end
end end
def self.create_for_lesson(lesson_session, for_education) def self.create_for_lesson(lesson_session, for_education, split = nil, fee_rate = nil)
distribution = create(lesson_session, for_education) distribution = create(lesson_session, for_education, split, fee_rate)
distribution.lesson_session = lesson_session distribution.lesson_session = lesson_session
distribution.education = for_education distribution.education = for_education
# lock down the teacher_fee_in_cents
distribution.teacher_fee_in_cents = distribution.calculate_teacher_fee(split, fee_rate)
distribution distribution
end end
def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education) def self.create_for_lesson_package_purchase(lesson_package_purchase, for_education, split = nil, fee_rate = nil)
distribution = create(lesson_package_purchase, for_education) distribution = create(lesson_package_purchase, for_education, split, fee_rate)
distribution.lesson_package_purchase = lesson_package_purchase distribution.lesson_package_purchase = lesson_package_purchase
distribution.education = for_education distribution.education = for_education
# lock down the teacher_fee_in_cents
distribution.teacher_fee_in_cents = distribution.calculate_teacher_fee(split, fee_rate)
distribution distribution
end end
def self.create(target, education) def self.create(target, education, split, fee_rate)
distribution = TeacherDistribution.new distribution = TeacherDistribution.new
distribution.teacher = target.teacher distribution.teacher = target.teacher
distribution.ready = false distribution.ready = false
distribution.distributed = false distribution.distributed = false
distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education) distribution.amount_in_cents = target.lesson_booking.distribution_price_in_cents(target, education, split, fee_rate)
distribution.school = target.lesson_booking.school distribution.school = target.lesson_booking.school
distribution distribution
end end
@ -111,23 +120,34 @@ module JamRuby
(jamkazam_margin_in_cents / 100).round(2) (jamkazam_margin_in_cents / 100).round(2)
end end
def calculate_teacher_fee def calculate_teacher_fee(split = nil, fee_rate = nil)
if education if teacher_fee_in_cents
0 return teacher_fee_in_cents
else else
if is_test_drive? if education
0 0
else else
if school if is_test_drive?
# if school exists, use it's rate 0
rate = school.jamkazam_rate
else else
# otherwise use the teacher's rate
rate = teacher.teacher.jamkazam_rate if fee_rate
rate = (fee_rate * split) # charge_Fee is already handled elsewhere
else
if school
# if school exists, use it's rate
rate = school.jamkazam_rate + APP_CONFIG.stripe[:charge_fee]
else
# otherwise use the teacher's rate
rate = teacher.teacher.jamkazam_rate + APP_CONFIG.stripe[:charge_fee]
end
end
(amount_in_cents * rate).round
end end
(amount_in_cents * (rate + 0.03)).round # 0.03 is stripe fee that we include in cost of JK fee
end end
end end
end end
def student def student

View File

@ -93,7 +93,7 @@ module JamRuby
payment.amount_in_cents = payment.teacher_distribution.amount_in_cents payment.amount_in_cents = payment.teacher_distribution.amount_in_cents
payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee payment.fee_in_cents = payment.teacher_distribution.calculate_teacher_fee
effective_in_cents = payment.amount_in_cents - payment.fee_in_cents effective_in_cents = payment.real_distribution_in_cents
if payment.teacher_payment_charge.nil? if payment.teacher_payment_charge.nil?
charge = TeacherPaymentCharge.new charge = TeacherPaymentCharge.new

View File

@ -41,7 +41,8 @@ module JamRuby
:currency => "usd", :currency => "usd",
:customer => APP_CONFIG.stripe[:source_customer], :customer => APP_CONFIG.stripe[:source_customer],
:description => construct_description, :description => construct_description,
:metadata => metadata :metadata => metadata,
:destination => teacher.teacher.stripe_account_id
) )
stripe_charge stripe_charge

View File

@ -1912,6 +1912,13 @@ module JamRuby
stats stats
end end
def shopping_cart_total
total = 0
shopping_carts.each do |shopping_cart|
total += shopping_cart.product_info[:total_price]
end
total
end
def destroy_all_shopping_carts def destroy_all_shopping_carts
ShoppingCart.where("user_id=?", self).destroy_all ShoppingCart.where("user_id=?", self).destroy_all
end end
@ -2053,6 +2060,15 @@ module JamRuby
user_authorizations.where(provider: "stripe_connect").first user_authorizations.where(provider: "stripe_connect").first
end end
def paypal_auth
user_authorizations.where(provider: 'paypal').first
end
def has_paypal_auth?
auth = paypal_auth
auth && (!auth.token_expiration || auth.token_expiration > Time.now)
end
def has_stripe_connect? def has_stripe_connect?
auth = stripe_auth auth = stripe_auth
auth && (!auth.token_expiration || auth.token_expiration > Time.now) auth && (!auth.token_expiration || auth.token_expiration > Time.now)

View File

@ -784,6 +784,7 @@ FactoryGirl.define do
sequence(:publisher) { |n| "publisher-#{n}" } sequence(:publisher) { |n| "publisher-#{n}" }
sales_region 'United States' sales_region 'United States'
price 1.99 price 1.99
download_price 4.99
reproduction_royalty true reproduction_royalty true
public_performance_royalty true public_performance_royalty true
reproduction_royalty_amount 0.999 reproduction_royalty_amount 0.999

View File

@ -24,7 +24,7 @@ describe "Monthly Recurring Lesson Flow" do
teacher.stripe_account_id = stripe_account1_id teacher.stripe_account_id = stripe_account1_id
teacher.save! teacher.save!
} }
it "works" do it "works normal" do
# if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on # if it's later in the month, we'll make 2 lesson_package_purchases (prorated one, and next month's), which can throw off some assertions later on
Timecop.travel(Date.new(2016, 3, 20)) Timecop.travel(Date.new(2016, 3, 20))
@ -173,7 +173,7 @@ describe "Monthly Recurring Lesson Flow" do
booked_price = booking.booked_price booked_price = booking.booked_price
prorated = booked_price / 2 prorated = booked_price / 2
prorated_cents = (booked_price * 100).to_i prorated_cents = (prorated * 100).to_i
user.reload user.reload
user.lesson_purchases.length.should eql 1 user.lesson_purchases.length.should eql 1
lesson_purchase = user.lesson_purchases[0] lesson_purchase = user.lesson_purchases[0]
@ -205,10 +205,10 @@ describe "Monthly Recurring Lesson Flow" do
teacher_distribution.distributed.should be_true teacher_distribution.distributed.should be_true
TeacherPayment.count.should eql 1 TeacherPayment.count.should eql 1
payment = TeacherPayment.first payment = TeacherPayment.first
payment.amount_in_cents.should eql 3000 payment.amount_in_cents.should eql prorated_cents
payment.fee_in_cents.should eql (3000 * 0.28).round payment.fee_in_cents.should eql (prorated_cents * 0.28).round
payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.amount_in_cents.should eql (payment.real_distribution_in_cents + payment.real_distribution_in_cents * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round payment.teacher_payment_charge.fee_in_cents.should eql (prorated_cents * 0.28).round
payment.teacher.should eql teacher_user payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql teacher_distribution payment.teacher_distribution.should eql teacher_distribution
@ -405,7 +405,7 @@ describe "Monthly Recurring Lesson Flow" do
booked_price = booking.booked_price booked_price = booking.booked_price
prorated = booked_price / 2 prorated = booked_price / 2
prorated_cents = (booked_price * 100).to_i prorated_cents = (prorated * 100).to_i
user.reload user.reload
user.lesson_purchases.length.should eql 1 user.lesson_purchases.length.should eql 1
lesson_purchase = user.lesson_purchases[0] lesson_purchase = user.lesson_purchases[0]
@ -441,16 +441,16 @@ describe "Monthly Recurring Lesson Flow" do
teacher_distribution.distributed.should be_true teacher_distribution.distributed.should be_true
TeacherPayment.count.should eql 2 TeacherPayment.count.should eql 2
payment = teacher_distribution.teacher_payment payment = teacher_distribution.teacher_payment
payment.amount_in_cents.should eql 3000 payment.amount_in_cents.should eql prorated_cents
payment.fee_in_cents.should eql (3000 * 0.28).round payment.fee_in_cents.should eql (prorated_cents * 0.28).round
payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.amount_in_cents.should eql (payment.real_distribution_in_cents + payment.real_distribution_in_cents * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round payment.teacher_payment_charge.fee_in_cents.should eql (prorated_cents * 0.28).round
payment.teacher.should eql teacher_user payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql teacher_distribution payment.teacher_distribution.should eql teacher_distribution
education_distribution.reload education_distribution.reload
education_distribution.distributed.should be_true education_distribution.distributed.should be_true
education_amt = (3000 * 0.0625).round education_amt = (prorated_cents * 0.0625).round
payment = education_distribution.teacher_payment payment = education_distribution.teacher_payment
payment.amount_in_cents.should eql education_amt payment.amount_in_cents.should eql education_amt
payment.fee_in_cents.should eql 0 payment.fee_in_cents.should eql 0

View File

@ -15,6 +15,7 @@ describe "Normal Lesson Flow" do
let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) } let(:affiliate_partner) { FactoryGirl.create(:affiliate_partner) }
let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) } let(:affiliate_partner2) { FactoryGirl.create(:affiliate_partner, lesson_rate: 0.30) }
let(:school) {FactoryGirl.create(:school)} let(:school) {FactoryGirl.create(:school)}
let(:retailer) {FactoryGirl.create(:retailer)}
after {Timecop.return} after {Timecop.return}
@ -459,6 +460,7 @@ describe "Normal Lesson Flow" do
booking.card_presumed_ok.should be_false booking.card_presumed_ok.should be_false
booking.user.should eql user booking.user.should eql user
user.unprocessed_normal_lesson.should be_nil user.unprocessed_normal_lesson.should be_nil
booking.same_school_free.should be_true
booking.sent_notices.should be_true booking.sent_notices.should be_true
booking.booked_price.should eql 30.00 booking.booked_price.should eql 30.00
booking.is_requested?.should be_true booking.is_requested?.should be_true
@ -609,8 +611,8 @@ describe "Normal Lesson Flow" do
booking.school.should be_true booking.school.should be_true
booking.card_presumed_ok.should be_false booking.card_presumed_ok.should be_false
booking.user.should eql user booking.user.should eql user
booking.same_school_free.should be_true booking.same_school_free.should be_false
user.unprocessed_normal_lesson.should be_nil #user.unprocessed_normal_lesson.should be_nil
booking.sent_notices.should be_false booking.sent_notices.should be_false
booking.booked_price.should eql 30.00 booking.booked_price.should eql 30.00
booking.is_requested?.should be_true booking.is_requested?.should be_true
@ -793,15 +795,236 @@ describe "Normal Lesson Flow" do
payment = teacher_distribution.teacher_payment payment = teacher_distribution.teacher_payment
payment.amount_in_cents.should eql 3000 payment.amount_in_cents.should eql 3000
payment.fee_in_cents.should eql (3000 * 0.28).round payment.fee_in_cents.should eql (3000 * 0.28).round
payment.teacher_payment_charge.amount_in_cents.should eql (3000 + 3000 * APP_CONFIG.stripe[:ach_pct]).round payment.teacher_payment_charge.amount_in_cents.should eql ((teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) + (teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql (3000 * 0.28).round payment.teacher_payment_charge.fee_in_cents.should eql payment.teacher_payment_charge.fee_in_cents
payment.teacher.should eql teacher_user payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql teacher_distribution payment.teacher_distribution.should eql teacher_distribution
lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED
lesson_session.lesson_booking.success.should be_true lesson_session.lesson_booking.success.should be_true
end
it "works (retailer on retailer)" do
# make sure teacher can get payments
teacher.stripe_account_id = stripe_account1_id
retailer.user.stripe_account_id = stripe_account2_id
# make sure can get stripe payments
# get user and teacher into same retailer
teacher_split = 70
retailer_split = 30
teacher_split_pct = (teacher_split / 100.0)
retailer_split_pct = (retailer_split / 100.0)
retailer.update_payment({teacher: teacher_split, retailer:retailer_split})
retailer.save!
user.affiliate_referral = retailer.affiliate_partner
user.save!
teacher.retailer = retailer
teacher.save!
# user has no test drives, no credit card on file, but attempts to book a lesson
booking = LessonBooking.book_normal(user, teacher_user, valid_single_slots, "Hey I've heard of you before.", false, LessonBooking::PAYMENT_STYLE_SINGLE, 60)
booking.errors.any?.should be_false
booking.retailer.should eql retailer
booking.card_presumed_ok.should be_false
booking.user.should eql user
booking.same_school_free.should be_false
booking.same_retailer.should be_true
#user.unprocessed_normal_lesson.should be_nil
booking.sent_notices.should be_false
booking.booked_price.should eql 30.00
booking.is_requested?.should be_true
booking.lesson_sessions[0].music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
LessonPaymentCharge.count.should eql 1
########## Need validate their credit card
token = create_stripe_token
result = user.payment_update({token: token, zip: '78759', normal: true, booking_id: booking.id})
booking = result[:lesson]
lesson = booking.lesson_sessions[0]
booking.errors.any?.should be_false
lesson.errors.any?.should be_false
booking.card_presumed_ok.should be_true
booking.sent_notices.should be_true
lesson.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
lesson.amount_charged.should eql 0.0
lesson.reload
user.reload
user.stripe_customer_id.should_not be nil
user.remaining_test_drives.should eql 0
user.lesson_purchases.length.should eql 0
customer = Stripe::Customer.retrieve(user.stripe_customer_id)
customer.email.should eql user.email
booking.lesson_sessions.length.should eql 1
lesson_session = booking.lesson_sessions[0]
lesson_session.status.should eql LessonBooking::STATUS_REQUESTED
booking.status.should eql LessonBooking::STATUS_REQUESTED
######### Teacher counters with new slot
teacher_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 14)
UserMailer.deliveries.clear
lesson_session.counter({proposer: teacher_user, slot: teacher_countered_slot, message: 'Does this work?'})
booking.reload
booking.errors.any?.should be false
lesson_session.lesson_booking.errors.any?.should be false
lesson_session.lesson_booking_slots.length.should eql 1
lesson_session.lesson_booking_slots[0].proposer.should eql teacher_user
teacher_counter = lesson_session.lesson_booking_slots.order(:created_at).last
teacher_counter.should eql teacher_countered_slot
teacher_counter.proposer.should eql teacher_user
booking.lesson_booking_slots.length.should eql 3
UserMailer.deliveries.length.should eql 1
chat = ChatMessage.unscoped.order(:created_at).last
chat.channel.should eql ChatMessage::CHANNEL_LESSON
chat.message.should eql 'Does this work?'
chat.user.should eql teacher_user
chat.target_user.should eql user
notification = Notification.unscoped.order(:created_at).last
notification.session_id.should eql lesson_session.music_session.id
notification.student_directed.should eql true
notification.purpose.should eql 'counter'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
######### Student counters with new slot
student_countered_slot = FactoryGirl.build(:lesson_booking_slot_single, hour: 16)
UserMailer.deliveries.clear
lesson_session.counter({proposer: user, slot: student_countered_slot, message: 'Does this work better?'})
lesson_session.errors.any?.should be false
lesson_session.lesson_booking.errors.any?.should be false
lesson_session.lesson_booking_slots.length.should eql 2
student_counter = booking.lesson_booking_slots.order(:created_at).last
student_counter.proposer.should eql user
booking.reload
booking.lesson_booking_slots.length.should eql 4
UserMailer.deliveries.length.should eql 1
chat = ChatMessage.unscoped.order(:created_at).last
chat.message.should eql 'Does this work better?'
chat.channel.should eql ChatMessage::CHANNEL_LESSON
chat.user.should eql user
chat.target_user.should eql teacher_user
notification = Notification.unscoped.order(:created_at).last
notification.session_id.should eql lesson_session.music_session.id
notification.student_directed.should eql false
notification.purpose.should eql 'counter'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
######## Teacher accepts slot
UserMailer.deliveries.clear
lesson_session.accept({message: 'Yeah I got this', slot: student_counter.id, update_all: false, accepter: teacher_user})
lesson_session.errors.any?.should be_false
lesson_session.reload
lesson_session.slot.should eql student_counter
lesson_session.status.should eql LessonSession::STATUS_APPROVED
booking.reload
booking.default_slot.should eql student_counter
lesson_session.music_session.scheduled_start.should eql booking.default_slot.scheduled_time(0)
booking.status.should eql LessonBooking::STATUS_APPROVED
UserMailer.deliveries.length.should eql 2
chat = ChatMessage.unscoped.order(:created_at).last
chat.message.should eql 'Yeah I got this'
chat.purpose.should eql 'Lesson Approved'
chat.channel.should eql ChatMessage::CHANNEL_LESSON
chat.user.should eql teacher_user
chat.target_user.should eql user
notification = Notification.unscoped.order(:created_at).last
notification.session_id.should eql lesson_session.music_session.id
notification.student_directed.should eql true
notification.purpose.should eql 'accept'
notification.description.should eql NotificationTypes::LESSON_MESSAGE
# teacher & student get into session
start = lesson_session.scheduled_start
end_time = lesson_session.scheduled_start + (60 * lesson_session.duration)
uh2 = FactoryGirl.create(:music_session_user_history, user: teacher_user, history: lesson_session.music_session, created_at: start, session_removed_at: end_time)
# artificially end the session, which is covered by other background jobs
lesson_session.music_session.session_removed_at = end_time
lesson_session.music_session.save!
Timecop.travel(end_time + 1)
UserMailer.deliveries.clear
# background code comes around and analyses the session
LessonSession.hourly_check
lesson_session.reload
lesson_session.analysed.should be_true
analysis = lesson_session.analysis
analysis["reason"].should eql LessonSessionAnalyser::STUDENT_FAULT
analysis["student"].should eql LessonSessionAnalyser::NO_SHOW
lesson_session.billed.should be_true
if lesson_session.billing_error_detail
puts "testdrive flow #{lesson_session.billing_error_detail}" # this should not occur, but helps a great deal if a regression occurs and running all the tests
end
lesson_session.billing_attempts.should eql 1
user.reload
user.lesson_purchases.length.should eql 1
LessonBooking.hourly_check
lesson_session.reload
lesson_session.education_distribution.should be_nil
teacher_distribution = lesson_session.teacher_distribution
teacher_distribution.amount_in_cents.should eql (3000 * teacher_split_pct).round
teacher_distribution.teacher_fee_in_cents.should eql (teacher_distribution.amount_in_cents * (teacher_split_pct * (retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee]))).round
teacher_distribution.ready.should be_true
teacher_distribution.distributed.should be_false
lesson_session.teacher_distributions.count.should eql 2
retailer_distribution = lesson_session.retailer_distribution
retailer_distribution.amount_in_cents.should eql (3000 * retailer_split_pct).round
retailer_distribution.teacher_fee_in_cents.should eql (retailer_distribution.amount_in_cents * (retailer_split_pct * (retailer.jamkazam_rate + APP_CONFIG.stripe[:charge_fee]))).round
retailer_distribution.ready.should be_true
retailer_distribution.distributed.should be_false
lesson_session.billed.should be true
user.reload
user.lesson_purchases.length.should eql 1
user.sales.length.should eql 1
lesson_session.amount_charged.should eql 32.48
lesson_session.billing_error_reason.should be_nil
lesson_session.sent_billing_notices.should be_true
user.reload
user.remaining_test_drives.should eql 0
UserMailer.deliveries.length.should eql 2 # one for student, one for teacher
TeacherPayment.count.should eql 0
TeacherPayment.hourly_check
TeacherPayment.count.should eql 2
LessonPaymentCharge.count.should eql 1
TeacherDistribution.count.should eql 2
teacher_distribution.reload
teacher_distribution.distributed.should be_true
retailer_distribution.reload
retailer_distribution.distributed.should be_true
retailer_amt = (3000 * retailer_split_pct).round
payment = retailer_distribution.teacher_payment
payment.amount_in_cents.should eql retailer_distribution.amount_in_cents
payment.fee_in_cents.should eql retailer_distribution.teacher_fee_in_cents
payment.teacher_payment_charge.amount_in_cents.should eql ((retailer_distribution.amount_in_cents - retailer_distribution.teacher_fee_in_cents) + (retailer_distribution.amount_in_cents - retailer_distribution.teacher_fee_in_cents) * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql retailer_distribution.teacher_fee_in_cents
payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql retailer_distribution
payment = teacher_distribution.teacher_payment
payment.amount_in_cents.should eql teacher_distribution.amount_in_cents
payment.fee_in_cents.should eql teacher_distribution.teacher_fee_in_cents
payment.teacher_payment_charge.amount_in_cents.should eql ((teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) + (teacher_distribution.amount_in_cents - teacher_distribution.teacher_fee_in_cents) * APP_CONFIG.stripe[:ach_pct]).round
payment.teacher_payment_charge.fee_in_cents.should eql payment.teacher_payment_charge.fee_in_cents
payment.teacher.should eql teacher_user
payment.teacher_distribution.should eql teacher_distribution
lesson_session.lesson_booking.status.should eql LessonBooking::STATUS_COMPLETED
lesson_session.lesson_booking.success.should be_true
end end
it "affiliate gets their cut" do it "affiliate gets their cut" do

View File

@ -338,7 +338,7 @@ describe LessonBooking do
slot.day_of_week = jan1.wday slot.day_of_week = jan1.wday
booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60)
times = booking.predicted_times_for_month(next_year, 1) times = booking.predicted_times_for_month(next_year, 1)[:times]
times.length.should eql 5 times.length.should eql 5
times[0].to_date.should eql (jan1) times[0].to_date.should eql (jan1)
times[1].to_date.should eql (Date.new(next_year, 1, 8)) times[1].to_date.should eql (Date.new(next_year, 1, 8))
@ -357,7 +357,7 @@ describe LessonBooking do
slot.day_of_week = jan1.wday slot.day_of_week = jan1.wday
booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60) booking = LessonBooking.book_normal(user, teacher_user, valid_recurring_slots, "Hey I've heard of you before.", true, LessonBooking::PAYMENT_STYLE_WEEKLY, 60)
times = booking.predicted_times_for_month(next_year, 1) times = booking.predicted_times_for_month(next_year, 1)[:times]
times.length.should eql 3 times.length.should eql 3
times[0].to_date.should eql (Date.new(next_year, 1, 15)) times[0].to_date.should eql (Date.new(next_year, 1, 15))
times[1].to_date.should eql (Date.new(next_year, 1, 22)) times[1].to_date.should eql (Date.new(next_year, 1, 22))

View File

@ -890,29 +890,30 @@ describe MusicSession do
end end
end end
describe "purgeable sessions " do describe "purgeable sessions" do
it 'selects unscheduled sessions past due date' do it 'selects unscheduled sessions past due date' do
interval = MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE interval = MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE
dd = Time.now - (interval.to_i + 1).days
Timecop.travel(dd)
msess1 = FactoryGirl.create(:music_session) msess1 = FactoryGirl.create(:music_session)
dd = Time.now - (interval.to_i + 1).days
Timecop.travel(dd)
msess2 = FactoryGirl.create(:music_session) msess2 = FactoryGirl.create(:music_session)
purging = MusicSession.purgeable_sessions purging = MusicSession.purgeable_sessions
expect(purging.size).to be(1) expect(purging.size).to be(1)
expect(purging[0].id).to eq(msess1.id) expect(purging[0].id).to eq(msess2.id)
end end
it 'selects recurring and non-recurring sessions past due date' do it 'selects recurring and non-recurring sessions past due date' do
[MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE, [MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE,
MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE_RECUR].each do |interval| MusicSession::UNSTARTED_INTERVAL_DAYS_PURGE_RECUR].each do |interval|
Timecop.return
msess1 = FactoryGirl.create(:music_session, scheduled_start: Time.now)
dd = Time.now - (interval.to_i + 1).days dd = Time.now - (interval.to_i + 1).days
Timecop.travel(dd) Timecop.travel(dd)
msess1 = FactoryGirl.create(:music_session, scheduled_start: Time.now)
msess2 = FactoryGirl.create(:music_session, scheduled_start: Time.now) msess2 = FactoryGirl.create(:music_session, scheduled_start: Time.now)
purging = MusicSession.purgeable_sessions purging = MusicSession.purgeable_sessions
expect(purging.size).to be(1) expect(purging.size).to be(1)
expect(purging[0].id).to eq(msess1.id) expect(purging[0].id).to eq(msess2.id)
MusicSession.delete_all MusicSession.delete_all
end end
end end

View File

@ -20,7 +20,6 @@ describe Retailer do
end end
it "has correct associations" do it "has correct associations" do
retailer = FactoryGirl.create(:retailer) retailer = FactoryGirl.create(:retailer)
retailer.slug.should eql retailer.id
retailer.should eql retailer.user.owned_retailer retailer.should eql retailer.user.owned_retailer

View File

@ -486,7 +486,7 @@ describe Sale do
purchase= adjustments[0] purchase= adjustments[0]
purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL)
purchase.description.should eq("JamTrack: " + jamtrack.name) purchase.description.should eq("JamTrack: " + jamtrack.name + '- FOR USE ONLY WITHIN APP')
purchase.state.should eq('invoiced') purchase.state.should eq('invoiced')
purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)

View File

@ -32,11 +32,11 @@ describe ShoppingCart do
end end
it "allows mix of free and not free stuff" do it "allows mix of free and not free stuff" do
cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
cart1.should_not be_nil cart1.should_not be_nil
cart1.errors.any?.should be_false cart1.errors.any?.should be_false
user.reload user.reload
cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
cart2.errors.any?.should be_false cart2.errors.any?.should be_false
user.reload user.reload
user.shopping_carts.length.should eq(1) user.shopping_carts.length.should eq(1)
@ -44,7 +44,7 @@ describe ShoppingCart do
cart3.errors.any?.should be_false cart3.errors.any?.should be_false
user.reload user.reload
user.shopping_carts.length.should eq(2) user.shopping_carts.length.should eq(2)
cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) cart4 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
cart4.errors.any?.should be_false cart4.errors.any?.should be_false
user.reload user.reload
user.shopping_carts.length.should eq(3) user.shopping_carts.length.should eq(3)
@ -70,12 +70,12 @@ describe ShoppingCart do
it "removes redeemable item to shopping cart (maintains only one in cart)" do it "removes redeemable item to shopping cart (maintains only one in cart)" do
user.has_redeemable_jamtrack.should be_true user.has_redeemable_jamtrack.should be_true
cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track, clear: true) cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
cart1.should_not be_nil cart1.should_not be_nil
cart1.errors.any?.should be_false cart1.errors.any?.should be_false
cart1.marked_for_redeem.should eq(1) cart1.marked_for_redeem.should eq(1)
user.reload user.reload
cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2, clear: true) cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track2)
cart2.should_not be_nil cart2.should_not be_nil
cart2.errors.any?.should be_false cart2.errors.any?.should be_false
cart2.marked_for_redeem.should eq(1) cart2.marked_for_redeem.should eq(1)

View File

@ -265,7 +265,7 @@ describe TeacherPayment do
teacher_distribution = payment.teacher_payment_charge.distribution teacher_distribution = payment.teacher_payment_charge.distribution
teacher_distribution.amount_in_cents.should eql 1000 teacher_distribution.amount_in_cents.should eql 1000
charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id) charge = Stripe::Charge.retrieve(payment.teacher_payment_charge.stripe_charge_id)
charge.destination.should be_nil charge.destination.should_not be_nil
charge.amount.should eql 726 charge.amount.should eql 726
charge.application_fee.should be_nil charge.application_fee.should be_nil
end end

View File

@ -306,7 +306,8 @@ def app_config
:publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX', :publishable_key => 'pk_test_HLTvioRAxN3hr5fNfrztZeoX',
:secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D', :secret_key => 'sk_test_OkjoIF7FmdjunyNsdVqJD02D',
:source_customer => 'cus_88Vp44SLnBWMXq', # seth@jamkazam.com in JamKazam-test account :source_customer => 'cus_88Vp44SLnBWMXq', # seth@jamkazam.com in JamKazam-test account
:ach_pct => 0.008 :ach_pct => 0.008,
:charge_fee => 0.03
} }
end end
def musician_count def musician_count

View File

@ -28,6 +28,8 @@ gem 'sprockets-rails', '2.3.2'
gem 'non-stupid-digest-assets' gem 'non-stupid-digest-assets'
#gem 'license_finder' #gem 'license_finder'
gem 'pg_migrate', '0.1.14' gem 'pg_migrate', '0.1.14'
#gem 'paypal-sdk-rest'
gem 'paypal-sdk-merchant-jk', '1.118.1'
gem 'kickbox' gem 'kickbox'
gem 'oj', '2.10.2' gem 'oj', '2.10.2'
gem 'builder' gem 'builder'
@ -70,7 +72,7 @@ gem 'filepicker-rails', '0.1.0'
gem 'aws-sdk', '~> 1' gem 'aws-sdk', '~> 1'
gem 'aasm' #, '3.0.16' gem 'aasm' #, '3.0.16'
gem 'carmen' gem 'carmen'
gem 'carrierwave' #, '0.9.0' gem 'carrierwave', '0.11.2' #, '0.9.0'
gem 'carrierwave_direct' gem 'carrierwave_direct'
gem 'fog' gem 'fog'
#gem 'jquery-payment-rails', github: 'sethcall/jquery-payment-rails' #gem 'jquery-payment-rails', github: 'sethcall/jquery-payment-rails'
@ -163,7 +165,7 @@ end
gem 'sass-rails' gem 'sass-rails'
gem 'coffee-rails' gem 'coffee-rails'
gem 'uglifier' gem 'uglifier'
gem 'coffee-script-source', '1.11.1'
group :test, :cucumber do group :test, :cucumber do
gem 'simplecov', '~> 0.7.1' gem 'simplecov', '~> 0.7.1'
gem 'simplecov-rcov' gem 'simplecov-rcov'

View File

@ -27,7 +27,7 @@
//= require jquery.Jcrop //= require jquery.Jcrop
//= require jquery.naturalsize //= require jquery.naturalsize
//= require jquery.queryparams //= require jquery.queryparams
//= require jquery.clipboard //= require clipboard
//= require jquery.timeago //= require jquery.timeago
//= require jquery.easydropdown //= require jquery.easydropdown
//= require jquery.scrollTo //= require jquery.scrollTo

View File

@ -65,6 +65,8 @@
return; return;
} }
console.log("GetLocalRecordingState", localResults)
$.each(claimedRecordings, function(index, claimedRecording) { $.each(claimedRecordings, function(index, claimedRecording) {
var options = { var options = {

View File

@ -10,6 +10,8 @@
var $dialog = null; var $dialog = null;
var $saveVideoCheckbox = null var $saveVideoCheckbox = null
var $uploadToYoutube = null var $uploadToYoutube = null
var timeout = null
var CLIENT_ROLE = context.JK.CLIENT_ROLE
function resetForm() { function resetForm() {
// remove all display errors // remove all display errors
@ -44,6 +46,47 @@
resetForm(); resetForm();
if(context.jamClient.getClientParentChildRole() == CLIENT_ROLE.CHILD) {
logger.debug("child client; launching preview after xfer");
$('#recording-finished-dialog span.nowait').addClass('hidden')
$('#recording-finished-dialog span.pleasewait').removeClass('hidden')
$('#recording-finished-dialog .preview-area').css('visibility', 'hidden')
$('#recording-finished-dialog form').css('visibility', 'hidden')
waitForMixTransfer()
}
else {
console.log("normal client; launching preview immediately")
$('#recording-finished-dialog span.pleasewait').addClass('hidden')
$('#recording-finished-dialog span.nowait').removeClass('hidden')
$('#recording-finished-dialog .preview-area').css('visibility', 'visible')
$('#recording-finished-dialog form').css('visibility', 'visible')
launchPreview();
}
}
function waitForMixTransfer() {
timeout = setTimeout(function() {
console.log("checking for file transfer", window.RecordingStore.mixTransferred)
if(window.RecordingStore.mixTransferred) {
$('#recording-finished-dialog span.pleasewait').addClass('hidden')
$('#recording-finished-dialog span.nowait').removeClass('hidden')
$('#recording-finished-dialog .preview-area').css('visibility', 'visible')
$('#recording-finished-dialog form').css('visibility', 'visible')
timeout = null
launchPreview()
}
else {
waitForMixTransfer();
}
}, 1000)
}
function launchPreview() {
var parentSelector = '#recording-finished-dialog div.genre-selector'; var parentSelector = '#recording-finished-dialog div.genre-selector';
context.JK.GenreSelectorHelper.render(parentSelector); context.JK.GenreSelectorHelper.render(parentSelector);
@ -117,10 +160,12 @@
playbackControls.startMonitor(); playbackControls.startMonitor();
} }
} }
} }
function afterHide() { function afterHide() {
if(timeout) {
clearTimeout(timeout)
timeout = null
}
if(recording && recording.video) { if(recording && recording.video) {
var name = $('#recording-finished-dialog form input[name=name]').val(); var name = $('#recording-finished-dialog form input[name=name]').val();
name = name.replace(/[^A-Za-z0-9\-\ ]/g, ''); name = name.replace(/[^A-Za-z0-9\-\ ]/g, '');

View File

@ -11,6 +11,7 @@
var userDetail = null; var userDetail = null;
var entity = null; var entity = null;
var remainingCap = 140 - 22 - 1; // 140 tweet max, minus 22 for link size, minus 1 for space var remainingCap = 140 - 22 - 1; // 140 tweet max, minus 22 for link size, minus 1 for space
var clipboard = null;
function showSpinner() { function showSpinner() {
$(dialogId + ' .dialog-inner').hide(); $(dialogId + ' .dialog-inner').hide();
@ -444,27 +445,6 @@
function afterShow() { function afterShow() {
$("#shareType").text(entityType); $("#shareType").text(entityType);
if(context.JK.hasFlash()) {
$("#btn-share-copy").clipboard({
path: '/assets/jquery.clipboard.swf',
copy: function() {
// Return text in closest element (useful when you have multiple boxes that can be copied)
return $(".link-contents").text();
}
});
}
else {
if(context.jamClient) {
// uses bridge call to ultimately access QClipboard
$("#btn-share-copy").unbind('click').click(function() {
context.jamClient.SaveToClipboard($(".link-contents").text());
return false;
})
}
else {
logger.debug("no copy-to-clipboard capabilities")
}
}
} }
function afterHide() { function afterHide() {
@ -486,6 +466,20 @@
//initDialog(); //initDialog();
facebookHelper.deferredLoginStatus().done(function(response) { handleFbStateChange(response); }); facebookHelper.deferredLoginStatus().done(function(response) { handleFbStateChange(response); });
if(context.jamClient.IsNativeClient()) {
$("#btn-share-copy").unbind('click').click(function() {
context.jamClient.SaveToClipboard($("#link-contents").text());
return false;
})
}
else {
clipboard = new Clipboard('#btn-share-copy', {
text: function(trigger) {
return $("#link-contents").text();
}
})
}
} }
this.initialize = initialize; this.initialize = initialize;

View File

@ -188,6 +188,37 @@
return context.JK.prodBubble($element, 'teacher-profile', {}, bigHelpDarkOptions({spikeGirth:0, spikeLength: 0, duration:10000, offsetParent:$offsetParent, width:385, positions:['top', 'right', 'bottom']})) return context.JK.prodBubble($element, 'teacher-profile', {}, bigHelpDarkOptions({spikeGirth:0, spikeLength: 0, duration:10000, offsetParent:$offsetParent, width:385, positions:['top', 'right', 'bottom']}))
} }
helpBubble.jamtrackVariants = function($element, $offsetParent) {
var offer = function() {
console.log("jamtrackVariant turn off")
$element.btOff()
$offsetParent.off('click', offer)
}
var bubble = context.JK.prodBubble($element, 'jamtrack-variants', {}, bigHelpDarkOptions({clickAnywhereToClose: true, spikeGirth:0, spikeLength: 0, duration:20000, positions:['bottom', 'right', 'left'], offsetParent: $offsetParent}))
setTimeout(function() {
$offsetParent.on('click', offer)
}, 1)
return bubble
}
helpBubble.jamtrackUpgrade = function($element, $offsetParent) {
var offer = function() {
console.log("jamtrackUpgrade turn off")
$element.btOff()
$offsetParent.off('click', offer)
}
var bubble = context.JK.prodBubble($element, 'jamtrack-upgrade', {}, bigHelpDarkOptions({clickAnywhereToClose: true, spikeGirth:0, spikeLength: 0, duration:20000, positions:['bottom', 'right', 'left'], offsetParent: $offsetParent}))
setTimeout(function() {
$offsetParent.on('click', offer)
}, 1)
return bubble
}
helpBubble.showUseRemainingTestDrives = function($element, $offsetParent, user, callback) { helpBubble.showUseRemainingTestDrives = function($element, $offsetParent, user, callback) {
return context.JK.onceBubble($element, 'side-remaining-test-drives', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) { return context.JK.onceBubble($element, 'side-remaining-test-drives', user, {offsetParent:$offsetParent, width:260, positions:['right'], postShow: function(container) {

View File

@ -2734,6 +2734,27 @@
}) })
} }
function paypalDetail(options) {
options = options || {}
return $.ajax({
type: 'POST',
url: '/api/paypal/checkout/detail',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function paypalPlaceOrder(options) {
options = options || {}
return $.ajax({
type: 'POST',
url: '/api/paypal/checkout/confirm',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function initialize() { function initialize() {
return self; return self;
@ -2977,6 +2998,8 @@
this.posaActivate = posaActivate; this.posaActivate = posaActivate;
this.posaClaim = posaClaim; this.posaClaim = posaClaim;
this.sendRetailerCustomerEmail = sendRetailerCustomerEmail; this.sendRetailerCustomerEmail = sendRetailerCustomerEmail;
this.paypalDetail = paypalDetail;
this.paypalPlaceOrder = paypalPlaceOrder;
return this; return this;
}; };
})(window,jQuery); })(window,jQuery);

View File

@ -9,7 +9,7 @@
//= require jquery.queryparams //= require jquery.queryparams
//= require jquery.hoverIntent //= require jquery.hoverIntent
//= require jquery.cookie //= require jquery.cookie
//= require jquery.clipboard //= require clipboard
//= require jquery.easydropdown //= require jquery.easydropdown
//= require jquery.carousel-1.1 //= require jquery.carousel-1.1
//= require jquery.mousewheel-3.1.9 //= require jquery.mousewheel-3.1.9

View File

@ -496,7 +496,10 @@
monitoring = false; monitoring = false;
logger.debug("playbackControl.stopMonitor") logger.debug("playbackControl.stopMonitor")
if (monitorPlaybackTimeout != null) { if (monitorPlaybackTimeout != null) {
clearTimeout(monitorPlaybackTimeout); if(clearTimeout) {
clearTimeout(monitorPlaybackTimeout);
}
monitorPlaybackTimeout = null; monitorPlaybackTimeout = null;
} }
} }

View File

@ -92,6 +92,7 @@ profileUtils = context.JK.ProfileUtils
selected: 'account', selected: 'account',
updateErrors: null, updateErrors: null,
retailerName: null, retailerName: null,
teacherSplit: null,
teacherInvitations: null, teacherInvitations: null,
updating: false updating: false
} }
@ -121,18 +122,25 @@ profileUtils = context.JK.ProfileUtils
city = @root.find('select[name="cities"]').val() city = @root.find('select[name="cities"]').val()
password = @root.find('input[type="password"]').val() password = @root.find('input[type="password"]').val()
teacherSplit = @teacherSplit()
if teacherSplit
retailerSplit = Number((100 - teacherSplit).toFixed(2))
@setState(updating: true) @setState(updating: true)
rest.updateRetailer({ rest.updateRetailer({
id: this.state.retailer.id, id: this.state.retailer.id,
name: name, name: name,
state: region, state: region,
city: city, city: city,
password:password password:password,
split: {teacher: teacherSplit, retailer: retailerSplit}
}).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR)) }).done((response) => @onUpdateDone(response)).fail((jqXHR) => @onUpdateFail(jqXHR))
onUpdateDone: (response) -> onUpdateDone: (response) ->
@setState({retailer: response, retailerName: null, updateErrors: null, updating: false}) @setState({retailer: response, retailerName: null, teacherSplit: null, updateErrors: null, updating: false})
@app.layout.notify({title: "update success", text: "Your retailer information has been successfully updated"}) @app.layout.notify({title: "update success", text: "Your retailer information has been successfully updated"})
@ -262,6 +270,63 @@ profileUtils = context.JK.ProfileUtils
logger.debug("handleLocationChange #{country} #{region} ${city}") logger.debug("handleLocationChange #{country} #{region} ${city}")
@setState({city: city, region: region}) @setState({city: city, region: region})
teacherSplitCurrent: () ->
if this.state.teacherSplit?
console.log("taking state for teacher split")
this.state.teacherSplit
else
this.state.retailer.payment_details.teacher
teacherSplitValue: () ->
@teacherSplitCurrent()
retailerSplitValue: () ->
teacherSplit = @teacherSplitCurrent()
return (100 - teacherSplit).toFixed(2)
onTeacherBlur: () ->
teacherSplit = @root.find('input[name="teacher-split"]').val()
teacherSplit = Number(teacherSplit)
if teacherSplit != teacherSplit #NaN?
@setState({teacherSplit: null})
teacherSplit: () ->
teacherSplit = @root.find('input[name="teacher-split"]').val()
if teacherSplit
teacherSplit = Number(teacherSplit)
if !teacherSplit
console.log("defaulting to 100 because teachersplit is empty")
teacherSplit = 100
teacherSplit
onTeacherSplitChange: (e) ->
$target = $(e.target)
# edge cases first
teacherSplit = @root.find('input[name="teacher-split"]').val()
if teacherSplit == null || teacherSplit == ''
@setState({teacherSplit: ''})
return
teacherSplit = Number(teacherSplit)
if teacherSplit != teacherSplit # NaN?
console.log("teacher split is NaN; ignoring")
# do nothing; this way junk doesn't start showing up in retail square. Onblur will fix
return
teacherSplit = @teacherSplit()
if teacherSplit > 100
console.log("teacher split is > 100. setting to 100")
return
if teacherSplit < 0
console.log("teacher split is < 0. setting to 0")
return
@setState({teacherSplit: teacherSplit})
account: () -> account: () ->
nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors) nameErrors = context.JK.reactSingleFieldErrors('name', @state.updateErrors)
@ -313,6 +378,19 @@ profileUtils = context.JK.ProfileUtils
<StripeConnect purpose='retailer' user={this.state.user}/> <StripeConnect purpose='retailer' user={this.state.user}/>
</div> </div>
<div className="field split">
<div className="teacher-split">
<label>Teacher % of Each Lesson:</label>
<input name="teacher-split" className="split-input teacher" type="number" defaultValue="" placeholder="please enter a value 0-100" value={this.teacherSplitValue()} onChange={this.onTeacherSplitChange} onBlur={this.onTeacherBlur}/>
<span className="usage-hint">Enter 0-100</span>
</div>
<div className="retailer-split">
<label>Retailer % of Each Lesson:</label>
<input name="retailer-split" className="split-input retailer" type="number" defaultValue="" value={this.retailerSplitValue()} readonly={true} disabled={true}/>
<span className="usage-hint">This is computed automatically based on the Teacher %</span>
</div>
</div>
<div className="actions"> <div className="actions">
<a className={classNames(cancelClasses)} onClick={this.onCancel}>CANCEL</a> <a className={classNames(cancelClasses)} onClick={this.onCancel}>CANCEL</a>
<a className={classNames(updateClasses)} onClick={this.onUpdate}>UPDATE</a> <a className={classNames(updateClasses)} onClick={this.onUpdate}>UPDATE</a>

View File

@ -249,7 +249,7 @@ JamBlasterActions = @JamBlasterActions
audio: () -> audio: () ->
`<div className="audio-content"> `<div className="audio-content">
<JamBlasterTrackConfig /> To edit the JamBlaster audio settings, get into a session, and click the Settings link under My Tracks.
</div>` </div>`
ipSettingsChanged: (key, e) -> ipSettingsChanged: (key, e) ->

View File

@ -194,13 +194,14 @@ LessonTimerActions = context.LessonTimerActions
@app.ajaxError(jqXHR) @app.ajaxError(jqXHR)
cancelSelected: (lesson, recurring) -> cancelSelected: (lesson, recurring) ->
rest.checkLessonCancel({id: lesson.id, update_all: recurring}).done((response) => (@issueCancelLesson(lesson, rest.checkLessonCancel({id: lesson.id, update_all: recurring})
recurring))).fail((jqXHR) => (@cancelSelectedFail(jqXHR))) .done((response) => @issueCancelLesson(lesson, recurring))
.fail((jqXHR) => @cancelSelectedFail(jqXHR, lesson))
cancelSelectedFailed: (jqXHR) -> cancelSelectedFail: (jqXHR, lesson) ->
if jqXHR.status == 422 if jqXHR.status == 422
if recurring if lesson.recurring
if @viewerStudent() if @viewerStudent()
buttons = [] buttons = []
buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})

View File

@ -73,13 +73,37 @@ MIX_MODES = context.JK.MIX_MODES
actionBtn = null actionBtn = null
if jamtrack.purchased if jamtrack.purchased
actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="javascript:void(0)">PURCHASED</a>` if jamtrack.can_download
actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="javascript:void(0)">PURCHASED</a>`
else
priceNotice = `<div className={jamtrackPricesClasses}>$ {Number(jamtrack.upgrade_price).toFixed(2)}</div>`
actionBtn = `<div>
<div className="jamtrack-add-zone">
{priceNotice}
<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id} data-variant="download">UPGRADE TO FULL</a>
</div>
<a className="jamtrack-upgrade-help" href='#'>HELP</a>
</div>`
else if jamtrack.is_free else if jamtrack.is_free
actionBtn = `<a className="jamtrack-add-cart button-orange is_free" href="#" data-jamtrack-id={jamtrack.id}>GET IT FREE!</a>` actionBtn = `<a className="jamtrack-add-cart button-orange is_free" href="#" data-jamtrack-id={jamtrack.id} data-variant="full">GET IT FREE!</a>`
else if jamtrack.added_cart else if jamtrack.added_cart
actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="client#/shoppingCart">ALREADY IN CART</a>` actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="client#/shoppingCart">ALREADY IN CART</a>`
else else
actionBtn = `<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id}>ADD TO CART</a>` priceNotice = `<div className={jamtrackPricesClasses}>$ {jamtrack.price}</div>`
fullPriceNotice = `<div className={jamtrackPricesClasses}>$ {jamtrack.download_price}</div>`
actionBtn = `<div>
<div className="jamtrack-add-zone">
{priceNotice}
<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id} data-variant="stream">ADD TO CART</a>
</div>
<div className="jamtrack-add-zone">
{fullPriceNotice}
<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id} data-variant="full">ADD TO CART (FULL)</a>
</div>
<a className="jamtrack-variant-help" href='#'>HELP</a>
</div>`
availabilityNotice = null availabilityNotice = null
if jamtrack.sales_region==context.JK.AVAILABILITY_US if jamtrack.sales_region==context.JK.AVAILABILITY_US
@ -114,7 +138,6 @@ MIX_MODES = context.JK.MIX_MODES
<td className="jamtrack-action"> <td className="jamtrack-action">
<div className="jamtrack-action-container"> <div className="jamtrack-action-container">
<div className="jamtrack-actions"> <div className="jamtrack-actions">
<div className={jamtrackPricesClasses}>$ {jamtrack.price}</div>
{actionBtn} {actionBtn}
{availabilityNotice} {availabilityNotice}
</div> </div>
@ -213,7 +236,7 @@ MIX_MODES = context.JK.MIX_MODES
.done((response) => .done((response) =>
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count})
) )
.fail(() => .fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable' @app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false}) @setState({searching: false, first_search: false})
) )
@ -300,6 +323,7 @@ MIX_MODES = context.JK.MIX_MODES
e.preventDefault() e.preventDefault()
$target = $(e.target) $target = $(e.target)
params = id: $target.attr('data-jamtrack-id') params = id: $target.attr('data-jamtrack-id')
params.variant = $target.attr('data-variant')
isFree = $(e.target).is('.is_free') isFree = $(e.target).is('.is_free')
@rest.addJamtrackToShoppingCart(params).done((response) => @rest.addJamtrackToShoppingCart(params).done((response) =>
@ -322,6 +346,18 @@ MIX_MODES = context.JK.MIX_MODES
$parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack $parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack
$parent.find('.license-us-why').on 'click', @licenseUSWhy $parent.find('.license-us-why').on 'click', @licenseUSWhy
$parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded $parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded
$parent.find('.jamtrack-variant-help').on 'click', @showVariantHelp
$parent.find('.jamtrack-upgrade-help').on 'click', @showUpgradeHelp
showVariantHelp: (e) ->
$screen = $('#jamtrackFilter')
e.preventDefault()
context.JK.HelpBubbleHelper.jamtrackVariants($(e.target), $screen)
showUpgradeHelp: (e) ->
$screen = $('#jamtrackFilter')
e.preventDefault()
context.JK.HelpBubbleHelper.jamtrackUpgrade($(e.target), $screen)
toggleExpanded:(e) -> toggleExpanded:(e) ->
e.preventDefault() e.preventDefault()

View File

@ -95,13 +95,37 @@ MIX_MODES = context.JK.MIX_MODES
actionBtn = null actionBtn = null
if jamtrack.purchased if jamtrack.purchased
actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="javascript:void(0)">PURCHASED</a>` if jamtrack.can_download
actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="javascript:void(0)">PURCHASED</a>`
else
priceNotice = `<div className={jamtrackPricesClasses}>$ {Number(jamtrack.upgrade_price).toFixed(2)}</div>`
actionBtn = `<div>
<div className="jamtrack-add-zone">
{priceNotice}
<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id} data-variant="download">UPGRADE TO FULL</a>
</div>
<a className="jamtrack-upgrade-help" href='#'>HELP</a>
</div>`
else if jamtrack.is_free else if jamtrack.is_free
actionBtn = `<a className="jamtrack-add-cart button-orange is_free" href="#" data-jamtrack-id={jamtrack.id}>GET IT FREE!</a>` actionBtn = `<a className="jamtrack-add-cart button-orange is_free" href="#" data-jamtrack-id={jamtrack.id} data-variant="full">GET IT FREE!</a>`
else if jamtrack.added_cart else if jamtrack.added_cart
actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="client#/shoppingCart">ALREADY IN CART</a>` actionBtn = `<a className="jamtrack-add-cart-disabled button-grey button-disabled" href="client#/shoppingCart">ALREADY IN CART</a>`
else else
actionBtn = `<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id}>ADD TO CART</a>` priceNotice = `<div className={jamtrackPricesClasses}>$ {jamtrack.price}</div>`
fullPriceNotice = `<div className={jamtrackPricesClasses}>$ {jamtrack.download_price}</div>`
actionBtn = `<div>
<div className="jamtrack-add-zone">
{priceNotice}
<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id} data-variant="stream">ADD TO CART</a>
</div>
<div className="jamtrack-add-zone">
{fullPriceNotice}
<a className="jamtrack-add-cart button-orange" href="#" data-jamtrack-id={jamtrack.id} data-variant="full">ADD TO CART (FULL)</a>
</div>
<a className="jamtrack-variant-help" href='#'>HELP</a>
</div>`
availabilityNotice = null availabilityNotice = null
if jamtrack.sales_region==context.JK.AVAILABILITY_US if jamtrack.sales_region==context.JK.AVAILABILITY_US
@ -136,7 +160,6 @@ MIX_MODES = context.JK.MIX_MODES
<td className="jamtrack-action"> <td className="jamtrack-action">
<div className="jamtrack-action-container"> <div className="jamtrack-action-container">
<div className="jamtrack-actions"> <div className="jamtrack-actions">
<div className={jamtrackPricesClasses}>$ {jamtrack.price}</div>
{actionBtn} {actionBtn}
{availabilityNotice} {availabilityNotice}
</div> </div>
@ -326,12 +349,12 @@ MIX_MODES = context.JK.MIX_MODES
.done((response) => .done((response) =>
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count})
) )
.fail(() => .fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable' @app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false}) @setState({searching: false, first_search: false})
) )
) )
.fail(() => .fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable' @app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false}) @setState({searching: false, first_search: false})
) )
@ -440,6 +463,7 @@ MIX_MODES = context.JK.MIX_MODES
e.preventDefault() e.preventDefault()
$target = $(e.target) $target = $(e.target)
params = id: $target.attr('data-jamtrack-id') params = id: $target.attr('data-jamtrack-id')
params.variant = $target.attr('data-variant')
isFree = $(e.target).is('.is_free') isFree = $(e.target).is('.is_free')
@rest.addJamtrackToShoppingCart(params).done((response) => @rest.addJamtrackToShoppingCart(params).done((response) =>
@ -475,6 +499,18 @@ MIX_MODES = context.JK.MIX_MODES
$parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack $parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack
$parent.find('.license-us-why').on 'click', @licenseUSWhy $parent.find('.license-us-why').on 'click', @licenseUSWhy
$parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded $parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded
$parent.find('.jamtrack-variant-help').on 'click', @showVariantHelp
$parent.find('.jamtrack-upgrade-help').on 'click', @showUpgradeHelp
showVariantHelp: (e) ->
$screen = $('#jamtrackSearch')
e.preventDefault()
context.JK.HelpBubbleHelper.jamtrackVariants($(e.target), $screen)
showUpgradeHelp: (e) ->
$screen = $('#jamtrackSearch')
e.preventDefault()
context.JK.HelpBubbleHelper.jamtrackUpgrade($(e.target), $screen)
toggleExpanded:(e) -> toggleExpanded:(e) ->
e.preventDefault() e.preventDefault()

View File

@ -502,7 +502,7 @@ UserStore = context.UserStore
else if this.state.user.lesson_package_type_id == 'test-drive-2' else if this.state.user.lesson_package_type_id == 'test-drive-2'
explanation =`<span>You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 2 private online music lessons - 1 each from 2 different instructors in the JamClass instructor community. The price of this TestDrive package is $29.99.</span>` explanation =`<span>You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 2 private online music lessons - 1 each from 2 different instructors in the JamClass instructor community. The price of this TestDrive package is $29.99.</span>`
else else
alert("You do not have a test drive package selected") alert("You do not have a test drive package selected: " + this.state.user.lesson_package_type_id )
bookingDetail = `<p>{explanation} bookingDetail = `<p>{explanation}

View File

@ -0,0 +1,160 @@
context = window
MIX_MODES = context.JK.MIX_MODES
@PayPalConfirmationScreen = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
render: () ->
content = null
if this.state.sold
if context.jamClient && context.jamClient.IsNativeClient()
platformMessage = `<div>
<p> To play your purchased JamTrack, start a session and then open the JamTrack</p>
<a className="download-jamkazam-wrapper" href="/client#/createSession">
<div className="download-jamkazam">
Click Here to Start a Session
</div>
</a>
</div>`
else
platformMessage =
`<div>
<a href='/client#/jamtrack' className="download-jamkazam-wrapper jt-popup">
<div className="download-jamkazam">
Click Here to Start Using Your JamTrack
</div>
</a>
<a href='/downloads' rel="external" className="download-jamkazam-wrapper" target="_blank">
<div>
Do More With Your JamTrack - Click Here to Download Our Application
</div>
</a>
<a className="back-to-browsing" href="/client#/jamtrack">
or click here to browse more jamtracks
</a>
</div>`
content = `<div className="sold-notice">
<h2>Thank you for your order!</h2>
{platformMessage}
</div>`
else
orderButtons = {"button-orange": true, "place-order-btn": true, disabled: this.state.ordering }
cancelButtons = {"button-grey": true, "cancel": true, disabled: this.state.ordering }
content = `<div>
<h2 className="confirm-header">Confirm PayPal Payment</h2>
<p>You have not yet made a payment via PayPal. Please review your purchase and confirm or cancel.</p>
<div className="controls">
<a href="#" className={classNames(orderButtons)} onClick={this.placeOrder}>CONFIRM PURCHASE WITH
PAYPAL</a>
<a href="#" className={classNames(cancelButtons)} onClick={this.cancel}>CANCEL</a>
<div className="clearall"/>
</div>
<ShoppingCartContents carts={this.state.carts}/>
<div className="controls bottom">
<a href="#" className={classNames(orderButtons)} onClick={this.placeOrder}>CONFIRM PURCHASE WITH
PAYPAL</a>
<a href="#" className={classNames(cancelButtons)} onClick={this.cancel}>CANCEL</a>
<div className="clearall"/>
</div>
</div>`
`<div className="PayPalConfirmationScreen">
<div className="content-body-scroller">
{content}
</div>
</div>`
placeOrder: (e) ->
e.preventDefault()
if this.state.ordering
return
@setState({ordering: true})
console.log("placing order with paypal")
@rest.paypalPlaceOrder()
.done((response) =>
console.log("paypal detail obtained", response)
@setState({sold: true, ordering: false})
context.JK.JamTrackUtils.checkShoppingCart()
@app.refreshUser()
)
.fail((jqXHR) =>
@setState({ordering: false})
if jqXHR.status == 404
context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. <a href="/client#/checkoutPayment">Click Here to Checkout Again.</a>')
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message)
else
context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com')
)
cancelOrder: (e) ->
e.preventDefault()
window.location = '/client#/jamtrack'
getInitialState: () ->
{}
componentDidMount: () ->
componentDidUpdate: () ->
afterShow: (data) ->
rest.getShoppingCarts()
.done((carts) =>
@setState({carts: carts})
if carts.length == 0
window.location = '/client#/jamtrack'
return
@rest.paypalDetail()
.done((response) =>
console.log("paypal detail obtained", response)
)
.fail((jqXHR) =>
if jqXHR.status == 404
context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. <a href="/client#/checkoutPayment">Click Here to Checkout Again.</a>')
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message)
else
context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com')
@app.notifyServerError jqXHR, 'PayPal Communication Error'
)
)
.fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Unable to fetch carts'
)
beforeShow: () ->
this.setState({sold: false})
onAppInit: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
screenBindings =
'beforeShow': @beforeShow
'afterShow': @afterShow
@app.bindScreen('paypal/confirm', screenBindings)
onUserChanged: (userState) ->
@user = userState?.user
})

View File

@ -69,7 +69,7 @@ JamTrackPlayerStore = reactContext.JamTrackPlayerStore
new window.Fingerprint2().get((result, components) => ( new window.Fingerprint2().get((result, components) => (
redirectTo = "/api/mixdowns/#{@state.mixdown.id}/download.mp3?file_type=mp3&sample_rate=#{@sampleRate}&download=1&mark=#{result}" redirectTo = "/api/mixdowns/#{@state.mixdown.id}/download.mp3?file_type=mp3&sample_rate=#{@sampleRate}&download=1&mark=#{result}"
redirectTo = URI.escape(redirectTo) redirectTo = encodeURIComponent(redirectTo)
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}")
)) ))

View File

@ -6,7 +6,6 @@ rest = context.JK.Rest()
mixins = [] mixins = []
# make sure this is actually us opening the window, not someone else (by checking for MixerStore) # make sure this is actually us opening the window, not someone else (by checking for MixerStore)
# this check ensures we attempt to listen if this component is created in a popup # this check ensures we attempt to listen if this component is created in a popup
reactContext = if window.opener? then window.opener else window reactContext = if window.opener? then window.opener else window
@ -38,7 +37,8 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
weight = switch weight = switch
when jam_track_track.track_type == 'Master' then 0 when jam_track_track.track_type == 'Master' then 0
when jam_track_track.track_type == 'Click' then 10000 when jam_track_track.track_type == 'Click' then 10000
else jam_track_track.position else
jam_track_track.position
onJamTrackPlayerStoreChanged: (changes) -> onJamTrackPlayerStoreChanged: (changes) ->
#logger.debug("PopupMediaControls: jamtrack changed", changes) #logger.debug("PopupMediaControls: jamtrack changed", changes)
@ -69,7 +69,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
AppActions.openExternalUrl($(e.target).attr('href')) AppActions.openExternalUrl($(e.target).attr('href'))
render: () -> render: () ->
closeLinkText = null closeLinkText = null
header = null header = null
extraControls = null extraControls = null
@ -90,17 +89,17 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
if selectedMixdown.client_state? if selectedMixdown.client_state?
switch selectedMixdown.client_state switch selectedMixdown.client_state
when 'download_fail' when 'download_fail'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>` customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png"/></h5>`
when 'downloading' when 'downloading'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>` customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif"/></h5>`
when 'ready' when 'ready'
customMixName = `<h5>{selectedMixdown.name}</h5>` customMixName = `<h5>{selectedMixdown.name}</h5>`
disabled = false disabled = false
else else
if selectedMixdown.myPackage if selectedMixdown.myPackage
customMixName = `<h5>Creating mixdown... <img src="/assets/shared/spinner.gif" /></h5>` customMixName = `<h5>Creating mixdown... <img src="/assets/shared/spinner.gif"/></h5>`
else else
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>` customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png"/></h5>`
else if selectedStem? else if selectedStem?
if selectedStem.instrument if selectedStem.instrument
@ -117,20 +116,20 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
if selectedStem.client_state? if selectedStem.client_state?
switch selectedStem.client_state switch selectedStem.client_state
when 'downloading' when 'downloading'
customMixName = `<h5>Loading {trackName}... <img src="/assets/shared/spinner.gif" /></h5>` customMixName = `<h5>Loading {trackName}... <img src="/assets/shared/spinner.gif"/></h5>`
when 'download_fail' when 'download_fail'
customMixName = `<h5>{trackName}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>` customMixName = `<h5>{trackName}<img src="/assets/content/icon-mix-fail@2X.png"/></h5>`
when 'ready' when 'ready'
customMixName = `<h5>{trackName}</h5>` customMixName = `<h5>{trackName}</h5>`
disabled = false disabled = false
else else
customMixName = `<h5>{trackName}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>` customMixName = `<h5>{trackName}<img src="/assets/content/icon-mix-fail@2X.png"/></h5>`
else else
if jamTrack?.client_state == 'downloading' if jamTrack?.client_state == 'downloading'
downloader = `<img src="/assets/shared/spinner.gif" />` downloader = `<img src="/assets/shared/spinner.gif"/>`
else if jamTrack?.client_state == 'download_fail' else if jamTrack?.client_state == 'download_fail'
downloader = `<img src="/assets/content/icon-mix-fail@2X.png" />` downloader = `<img src="/assets/content/icon-mix-fail@2X.png"/>`
jamTrackTypeHeader = `<span>Full JamTrack {downloader}</span>` jamTrackTypeHeader = `<span>Full JamTrack {downloader}</span>`
@ -153,15 +152,19 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
active = jamTrack.last_mixdown_id == null && jamTrack.last_stem_id == null active = jamTrack.last_mixdown_id == null && jamTrack.last_stem_id == null
myMixdowns.push ` myMixdowns.push `
<div key="full-track" className={classNames({'full-track': true, 'mixdown-display': true, 'active' : active})}> <div key="full-track"
className={classNames({'full-track': true, 'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name"> <div className="mixdown-name">
Full JamTrack Full JamTrack
</div> </div>
<div className="mixdown-actions"> <div className="mixdown-actions">
<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/> <img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>
<a href={this.createJamTrackUrl(jamTrack)} target="_blank" onClick={boundDownloadClick}><img src="/assets/content/icon_download@2X.png" className="mixdown-download"/></a> <a href={this.createJamTrackUrl(jamTrack)} target="_blank" onClick={boundDownloadClick}><img
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} /> src="/assets/content/icon_download@2X.png" className="mixdown-download"/></a>
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete" onClick={false} /> <img src="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit"
onClick={false}/>
<img src="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete"
onClick={false}/>
</div> </div>
</div>` </div>`
@ -186,26 +189,33 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
if mixdown_package if mixdown_package
switch mixdown_package.signing_state switch mixdown_package.signing_state
when 'QUIET_TIMEOUT' when 'QUIET_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>` action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play"
onClick={boundErrorClick}/>`
when 'QUIET' when 'QUIET'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>` action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED' when 'QUEUED'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>` action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED_TIMEOUT' when 'QUEUED_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>` action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play"
onClick={boundErrorClick}/>`
when 'SIGNING' when 'SIGNING'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>` action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'SIGNING_TIMEOUT' when 'SIGNING_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>` action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play"
onClick={boundErrorClick}/>`
when 'SIGNED' when 'SIGNED'
action = `<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>` action = `<img src="/assets/content/icon_open@2X.png" className="mixdown-play"
onClick={boundPlayClick}/>`
when 'ERROR' when 'ERROR'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>` action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play"
onClick={boundErrorClick}/>`
else else
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>` action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play"
onClick={boundErrorClick}/>`
if editing if editing
mixdownName = `<input className="edit-name" type="text" defaultValue={mixdown.name} onKeyDown={boundEditKeydown} />` mixdownName = `<input className="edit-name" type="text" defaultValue={mixdown.name}
onKeyDown={boundEditKeydown}/>`
editIcon = `<img src="/assets/content/icon-save@2X.png" className="mixdown-edit" onClick={boundSaveClick}/>` editIcon = `<img src="/assets/content/icon-save@2X.png" className="mixdown-edit" onClick={boundSaveClick}/>`
else else
mixdownName = mixdown.name mixdownName = mixdown.name
@ -213,9 +223,11 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
# create hidden objects to deal with alginment issues using a table # create hidden objects to deal with alginment issues using a table
if !editIcon if !editIcon
editIcon = `<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} />` editIcon = `<img src="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}}
className="mixdown-edit" onClick={false}/>`
download = `<a onClick={boundDownloadReadyClick} href={this.downloadMixdownUrl(mixdown)} target="_blank"><img src="/assets/content/icon_download@2X.png" className="mixdown-download" /></a>` download = `<a onClick={boundDownloadReadyClick} href={this.downloadMixdownUrl(mixdown)} target="_blank"><img
src="/assets/content/icon_download@2X.png" className="mixdown-download"/></a>`
myMixdowns.push ` myMixdowns.push `
<div key={mixdown.id} className={classNames({'mixdown-display': true, 'active' : active})}> <div key={mixdown.id} className={classNames({'mixdown-display': true, 'active' : active})}>
@ -227,7 +239,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
{download} {download}
{editIcon} {editIcon}
<img src ="/assets/content/icon-delete@2X.png" className="mixdown-delete" onClick={boundDeleteClick} /> <img src="/assets/content/icon-delete@2X.png" className="mixdown-delete" onClick={boundDeleteClick}/>
</div> </div>
</div>` </div>`
@ -260,16 +272,21 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
myMixdowns.push ` myMixdowns.push `
<div key={track.id} className={classNames({'stem-track' : true, 'mixdown-display': true, 'active' : active})}> <div key={track.id} className={classNames({'stem-track' : true, 'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name"> <div className="mixdown-name">
<select className="active-stem-select" name="active-stem-select" onChange={boundStemChange} defaultValue={jamTrack.last_stem_id}> <select className="active-stem-select" name="active-stem-select" onChange={boundStemChange}
defaultValue={jamTrack.last_stem_id}>
<option key="null" value="">or select a track</option> <option key="null" value="">or select a track</option>
{trackOptions} {trackOptions}
</select> </select>
</div> </div>
<div className="mixdown-actions"> <div className="mixdown-actions">
<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundStemActivateClick}/> <img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundStemActivateClick}/>
<a href={this.createStemUrl(jamTrack.id, jamTrack.last_stem_id)} target="_blank" onClick={boundStemPlayClick}><img src="/assets/content/icon_download@2X.png" className="mixdown-download" /></a> <a href={this.createStemUrl(jamTrack.id, jamTrack.last_stem_id)} target="_blank"
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} /> onClick={boundStemPlayClick}><img src="/assets/content/icon_download@2X.png"
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete" onClick={false} /> className="mixdown-download"/></a>
<img src="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit"
onClick={false}/>
<img src="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete"
onClick={false}/>
</div> </div>
</div>` </div>`
@ -300,23 +317,29 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
tracks.push(` tracks.push(`
<tr className="stem"> <tr className="stem">
<td><img src={context.JK.getInstrumentIcon24(instrumentId)} className="instrument-icon" /> {instrumentDescription} {part}</td> <td><img src={context.JK.getInstrumentIcon24(instrumentId)}
<td className="mute"><input type="checkbox" className="stem-mute" data-stem-id={track.id} defaultChecked={track.track_type == 'Click'}/></td> className="instrument-icon"/> {instrumentDescription} {part}</td>
<td className="mute"><input type="checkbox" className="stem-mute" data-stem-id={track.id}
defaultChecked={track.track_type == 'Click'}/></td>
</tr>`) </tr>`)
if jamTrack?.jmep?.Events? && jamTrack.jmep.Events[0].metronome? if jamTrack?.jmep?.Events? && jamTrack.jmep.Events[0].metronome?
# tap-in detected; show user tap-in option # tap-in detected; show user tap-in option
tracks.push(` tracks.push(`
<tr className="stem"> <tr className="stem">
<td><img src={context.JK.getInstrumentIcon24('computer')} className="instrument-icon" /> Count-in</td> <td><img src={context.JK.getInstrumentIcon24('computer')} className="instrument-icon"/> Count-in</td>
<td className="mute"><input type="checkbox" className="stem-mute" data-stem-id="count-in" /></td> <td className="mute"><input type="checkbox" className="stem-mute" data-stem-id="count-in"/></td>
</tr>`) </tr>`)
stems = `<div key="stems" className="stems"> stems = `<div key="stems" className="stems">
<table> <table>
<thead> <thead>
<col align="left" /><col align="right"/> <col align="left"/>
<tr><th>TRACKS</th><th className="mute">mute</th></tr> <col align="right"/>
<tr>
<th>TRACKS</th>
<th className="mute">mute</th>
</tr>
</thead> </thead>
<tbody> <tbody>
{tracks} {tracks}
@ -328,13 +351,15 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
nameClassData = {field: true} nameClassData = {field: true}
if @state.createMixdownErrors? if @state.createMixdownErrors?
errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'}) errorHtml = context.JK.reactErrors(@state.createMixdownErrors,
{name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'})
createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown}) createMixClasses = classNames({'button-orange': true, 'create-mix-btn': true, 'disabled': @state.creatingMixdown})
mixControls = ` mixControls = `
<div key="create-mix" className="create-mix"> <div key="create-mix" className="create-mix">
<p>Mute or unmute any tracks you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.</p> <p>Mute or unmute any tracks you like. You can also use the controls below to adjust the tempo or pitch of the
JamTrack. Then give your custom mix a name, and click the Create Mix button.</p>
{stems} {stems}
<div className="field"> <div className="field">
<label>TEMPO</label> <label>TEMPO</label>
@ -410,14 +435,22 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
if @state.showMyMixes if @state.showMyMixes
showMyMixesText = `<a className="show-hide-my-mixes" onClick={this.toggleMyMixes}>hide my mixes <div className="details-arrow arrow-up" /></a>` showMyMixesText = `<a className="show-hide-my-mixes" onClick={this.toggleMyMixes}>hide my mixes
<div className="details-arrow arrow-up"/>
</a>`
else else
showMyMixesText = `<a className="show-hide-my-mixes" onClick={this.toggleMyMixes}>show my mixes <div className="details-arrow arrow-down" /></a>` showMyMixesText = `<a className="show-hide-my-mixes" onClick={this.toggleMyMixes}>show my mixes
<div className="details-arrow arrow-down"/>
</a>`
if @state.showCustomMixes if @state.showCustomMixes
showMixControlsText = `<a className="show-hide-mix-controls" onClick={this.toggleCustomMixes}>hide mix controls <div className="details-arrow arrow-up" /></a>` showMixControlsText = `<a className="show-hide-mix-controls" onClick={this.toggleCustomMixes}>hide mix controls
<div className="details-arrow arrow-up"/>
</a>`
else else
showMixControlsText = `<a className="show-hide-mix-controls" onClick={this.toggleCustomMixes}>show mix controls <div className="details-arrow arrow-down" /></a>` showMixControlsText = `<a className="show-hide-mix-controls" onClick={this.toggleCustomMixes}>show mix controls
<div className="details-arrow arrow-down"/>
</a>`
extraControls = ` extraControls = `
<div className="extra-controls"> <div className="extra-controls">
@ -432,20 +465,65 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
</div>` </div>`
if helpLink? if helpLink?
helpButton = `<a className="help-link button-grey" href={helpLink} target="_blank">HELP</a>` helpButton = `<a className="help-link button-grey" href={helpLink} target="_blank">HELP</a>`
`<div className="media-controls-popup"> `<div className="jamtrack-player-main">
{header} <div className="jamtrack-player-controls">
<BrowserMediaControls disabled={this.disableLoading}/> <div className="media-header">
{extraControls} jamtracks web player
<div className="actions"> </div>
{helpButton} <div className="media-controls-popup">
{header}
<BrowserMediaControls disabled={this.disableLoading}/>
{extraControls}
<div className="actions">
{helpButton}
</div>
</div>
</div>
<div className="helpful-resources">
<div className="helpful-header">
helpful resources
</div>
<div className="helpful-section">
<a href="https://jamkazam.desk.com/customer/portal/articles/2166273-playing-your-jamtracks-in-a-browser" target="_blank">read a web player help article</a><br/>
that explains how to use all the features of this web player
</div>
<div className="helpful-section">
<a href="/client#/jamtrack" target="_blank">go to JamTracks homepage</a><br/>
where you can access all of your JamTracks, search for more JamTracks, and more
</div>
<div className="helpful-section">
<a href="https://itunes.apple.com/us/app/jamtracks/id1060927816?mt=8" target="_blank">download our free iOS app</a><br/>
that you can use to play with your JamTracks on your iPhone, iPad, or iPod Touch
</div>
<div className="helpful-section">
<a href="/downloads" target="_blank">download our free Mac or Windows app</a><br/>
that gives you more powerful features to do more amazing things with your JamTracks
</div>
<div className="helpful-section">
<a href="https://jamkazam.desk.com/customer/en/portal/articles/2126113-playing-your-jamtracks-in-our-free-windows-or-mac-app" target="_blank">review a list of help articles on the Mac/Win app</a><br/>
to understand all the things you can do with our free desktop app
</div>
<div className="helpful-section">
<a onClick={this.seeAllByArtist} target="_blank">see more JamTracks by this artist</a><br/>
to check out other songs you might like
</div>
<div className="helpful-section">
<a href="https://s3.amazonaws.com/jamkazam-public/public/lists/all-jamkazam-jamtracks.pdf" target="_blank">download our complete JamTracks catalog</a><br/>
to browse or search through all the music we have in a convenient PDF file
</div>
</div> </div>
</div>` </div>`
# <a className="close-link button-orange" onClick={this.close}>{closeLinkText}</a> seeAllByArtist: (e) ->
e.preventDefault()
jamTrack = @state.jamTrackState?.jamTrack
console.log("seeAllByArtist context", jamTrack)
window.open('/client?artist=' + encodeURIComponent(jamTrack.original_artist) + '#/jamtrack/search')
windowUnloaded: () -> windowUnloaded: () ->
JamTrackPlayerActions.windowUnloaded() unless window.DontAutoCloseMedia JamTrackPlayerActions.windowUnloaded() unless window.DontAutoCloseMedia
@ -464,14 +542,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
e.preventDefault() e.preventDefault()
verificationCheck: () -> verificationCheck: () ->
if @state.user?.email_needs_verification if @state.user?.email_needs_verification
alert("Check your email and click the verification link. Refresh this page when done, and try again.") alert("Check your email and click the verification link. Refresh this page when done, and try again.")
return @state.user?.email_needs_verification return @state.user?.email_needs_verification
downloadMixdownReady: (mixdown, e) -> downloadMixdownReady: (mixdown, e) ->
if @verificationCheck() if @verificationCheck()
e.preventDefault() e.preventDefault()
return return
@ -494,7 +570,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=#{result}" window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=#{result}"
activateStem: (e) -> activateStem: (e) ->
e.preventDefault() e.preventDefault()
return if @verificationCheck() return if @verificationCheck()
@ -506,7 +581,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
if !selectedTrackId? || selectedTrackId == '' if !selectedTrackId? || selectedTrackId == ''
alert("You must pick a track from the dropdown in order to play it.") alert("You must pick a track from the dropdown in order to play it.")
else else
@setState({editingMixdownId: null}) @setState({editingMixdownId: null})
e.preventDefault() e.preventDefault()
@ -518,8 +592,38 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
# make this package the active one # make this package the active one
JamTrackPlayerActions.openStem(selectedTrackId) JamTrackPlayerActions.openStem(selectedTrackId)
downloadStem: (e) -> addUpgradeToCart: (jamtrack) ->
console.log("adding upgrade to cart")
params = {id: jamtrack.id, variant: 'download'}
rest.addJamtrackToShoppingCart(params).done((response) =>
console.log("added item to shopping cart. fast_redeem? " + response.fast_redeem)
if response.fast_reedem
if context.JK.currentUserId?
window.open('/client#/redeemComplete')
else
window.open('/client#/redeemSignup')
else
window.open('/client#/shoppingCart')
).fail(((jqxhr) =>
handled = false
if jqxhr.status == 422
body = JSON.parse(jqxhr.responseText)
if body.errors?.cart_id?[0] == 'has already been taken'
console.log("already taken, just show shopping cart")
window.open('/client#/shoppingCart')
return
else if body.errors && body.errors.base
handled = true
alert("You can not have a mix of free and non-free items in your shopping cart.\n\nIf you want to add this new item to your shopping cart, then clear out all current items by clicking on the shopping cart icon and clicking 'delete' next to each item.")
if !handled
alert("Error adding to shoppig cart. " + jqxhr.responseText)
))
downloadStem: (e) ->
if @verificationCheck() if @verificationCheck()
e.preventDefault() e.preventDefault()
return return
@ -528,6 +632,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
selectedTrackId = $select.val() selectedTrackId = $select.val()
if !@state.jamTrackState.jamTrack.can_download
e.preventDefault()
if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?")
@addUpgradeToCart(@state.jamTrackState.jamTrack)
return
if !selectedTrackId? || selectedTrackId == '' if !selectedTrackId? || selectedTrackId == ''
e.preventDefault() e.preventDefault()
alert("You must select a track in order to download and also click the folder icon.") alert("You must select a track in order to download and also click the folder icon.")
@ -581,11 +691,17 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
JamTrackPlayerActions.activateNoMixdown(jamtrack) JamTrackPlayerActions.activateNoMixdown(jamtrack)
jamTrackDownload: (jamTrack, e) -> jamTrackDownload: (jamTrack, e) ->
if @verificationCheck() if @verificationCheck()
e.preventDefault() e.preventDefault()
return return
if !@state.jamTrackState.jamTrack.can_download
e.preventDefault()
if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?")
@addUpgradeToCart(@state.jamTrackState.jamTrack)
return
if /iPhone|iPad|iPod/i.test(navigator.userAgent) if /iPhone|iPad|iPod/i.test(navigator.userAgent)
# fall through # fall through
@ -622,14 +738,13 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
mixdownDelete: (mixdown) -> mixdownDelete: (mixdown) ->
if @state.editingMixdownId? if @state.editingMixdownId?
@setState({editingMixdownId:null}) @setState({editingMixdownId: null})
return return
if confirm("Delete this custom mix?") if confirm("Delete this custom mix?")
JamTrackPlayerActions.deleteMixdown(mixdown) JamTrackPlayerActions.deleteMixdown(mixdown)
mixdownError: (mixdown) -> mixdownError: (mixdown) ->
myPackage = mixdown.myPackage myPackage = mixdown.myPackage
if myPackage? if myPackage?
@ -709,7 +824,11 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
tracks.push({id: stemId, mute: muted}) tracks.push({id: stemId, mute: muted})
) )
mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch, "count-in": count_in, tracks:tracks}} mixdown = {
jamTrackID: @state.jamTrackState.jamTrack.id,
name: name,
settings: {speed: speed, pitch: pitch, "count-in": count_in, tracks: tracks}
}
JamTrackPlayerActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail) JamTrackPlayerActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail)
@ -743,7 +862,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
fetchUserInfo: () -> fetchUserInfo: () ->
rest.getUserDetail() rest.getUserDetail()
.done((response) => .done((response) =>
rest.postUserEvent({name: 'jamtrack_web_player_open'}) rest.postUserEvent({name: 'jamtrack_web_player_open'})
context.stats.write('web.jamtrack_web_player.open', { context.stats.write('web.jamtrack_web_player.open', {
value: 1, value: 1,
@ -757,13 +875,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
rest.userOpenedJamTrackWebPlayer() rest.userOpenedJamTrackWebPlayer()
$root = $(@getDOMNode()) $root = $(@getDOMNode())
#context.JK.prodBubble($root.find('.create-mix-btn'), 'first-time-jamtrack-web-player', {}, {positions:['left'], offsetParent: $root}) #context.JK.prodBubble($root.find('.create-mix-btn'), 'first-time-jamtrack-web-player', {}, {positions:['left'], offsetParent: $root})
), 1500) ), 1500)
) )
componentDidMount: () -> componentDidMount: () ->
$(window).unload(@windowUnloaded) $(window).unload(@windowUnloaded)
@root = jQuery(this.getDOMNode()) @root = jQuery(this.getDOMNode())

View File

@ -1,7 +1,7 @@
context = window context = window
logger = context.JK.logger logger = context.JK.logger
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
rest = context.JK.Rest()
mixins = [] mixins = []
@ -523,12 +523,49 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
return if @verificationCheck() return if @verificationCheck()
if !@state.jamTrackState.jamTrack.can_download
e.preventDefault()
if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?")
@addUpgradeToCart(@state.jamTrackState.jamTrack)
return
new window.Fingerprint2().get((result, components) => ( new window.Fingerprint2().get((result, components) => (
redirectTo = "/api/jamtracks/#{jamTrack.id}/stems/master/download.mp3?file_type=mp3&download=1&mark=#{result}" redirectTo = "/api/jamtracks/#{jamTrack.id}/stems/master/download.mp3?file_type=mp3&download=1&mark=#{result}"
redirectTo = URI.escape(redirectTo) redirectTo = encodeURIComponent(redirectTo)
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}")
)) ))
addUpgradeToCart: (jamtrack) ->
console.log("adding upgrade to cart")
params = {id: jamtrack.id, variant: 'download'}
rest.addJamtrackToShoppingCart(params).done((response) =>
console.log("added item to shopping cart. fast_redeem? " + response.fast_redeem)
if response.fast_reedem
if context.JK.currentUserId?
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/redeemComplete')
else
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/redeemSignup')
else
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/shoppingCart')
).fail(((jqxhr) =>
handled = false
if jqxhr.status == 422
body = JSON.parse(jqxhr.responseText)
if body.errors?.cart_id?[0] == 'has already been taken'
console.log("already taken, just show shopping cart")
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + '/client#/shoppingCart')
return
else if body.errors && body.errors.base
handled = true
alert("You can not have a mix of free and non-free items in your shopping cart.\n\nIf you want to add this new item to your shopping cart, then clear out all current items by clicking on the shopping cart icon and clicking 'delete' next to each item.")
if !handled
alert("Error adding to shoppig cart. " + jqxhr.responseText)
))
stemChanged:() -> stemChanged:() ->
stemDownload: (e) -> stemDownload: (e) ->
e.preventDefault() e.preventDefault()
@ -537,6 +574,12 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
$select = $(this.getDOMNode()).find('.active-stem-select') $select = $(this.getDOMNode()).find('.active-stem-select')
if !@state.jamTrackState.jamTrack.can_download
e.preventDefault()
if confirm("You have not purchased the rights to download a JamTrack. Add them to your shopping cart?")
@addUpgradeToCart(@state.jamTrackState.jamTrack)
return
selectedTrackId = $select.val() selectedTrackId = $select.val()
if !selectedTrackId? || selectedTrackId == '' if !selectedTrackId? || selectedTrackId == ''
@ -546,7 +589,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
new window.Fingerprint2().get((result, components) => ( new window.Fingerprint2().get((result, components) => (
redirectTo = "/api/jamtracks/#{@state.jamTrackState.jamTrack.id}/stems/#{selectedTrackId}/download.mp3?file_type=mp3&download=1&mark=#{result}" redirectTo = "/api/jamtracks/#{@state.jamTrackState.jamTrack.id}/stems/#{selectedTrackId}/download.mp3?file_type=mp3&download=1&mark=#{result}"
redirectTo = URI.escape(redirectTo) redirectTo = encodeURIComponent(redirectTo)
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}")
)) ))
@ -591,7 +634,7 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
if browserPackage?.signing_state == 'SIGNED' if browserPackage?.signing_state == 'SIGNED'
new window.Fingerprint2().get((result, components) => ( new window.Fingerprint2().get((result, components) => (
redirectTo = "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=#{result}" redirectTo = "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1&mark=#{result}"
redirectTo = URI.escape(redirectTo) redirectTo = encodeURIComponent(redirectTo)
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}") AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/signin?redirect-to=#{redirectTo}")
)) ))
else else
@ -716,7 +759,6 @@ mixins.push(Reflux.listenTo(UserStore, 'onUserChanged'))
setTimeout(@resizeWindow, 1000) setTimeout(@resizeWindow, 1000)
shouldComponentUpdate: () -> shouldComponentUpdate: () ->
console.log("THIS UNLOADED", @unloaded)
return !@unloaded return !@unloaded
resizeWindow: () => resizeWindow: () =>

View File

@ -36,6 +36,10 @@ if accessOpener
# this.setState(chatMixer: mixers.chatMixer) # this.setState(chatMixer: mixers.chatMixer)
onRecordingStateChanged: (recordingState) -> onRecordingStateChanged: (recordingState) ->
if @unloaded
#console.log("PopupMediaControls unloaded. ignore onMixersChnaged")
return
this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording) this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording)
startStopRecording: () -> startStopRecording: () ->
@ -165,6 +169,9 @@ if accessOpener
</div>` </div>`
windowUnloaded: () -> windowUnloaded: () ->
@unloaded = true
window.unloaded = true
window.opener.RecordingActions.recordingControlsClosed() window.opener.RecordingActions.recordingControlsClosed()
onChatHelp: (e) -> onChatHelp: (e) ->
@ -215,6 +222,9 @@ if accessOpener
$root = jQuery(this.getDOMNode()) $root = jQuery(this.getDOMNode())
$includeChat = $root.find('#include-chat') $includeChat = $root.find('#include-chat')
shouldComponentUpdate: () ->
return !@unloaded
resizeWindow: () => resizeWindow: () =>
$container = $('#minimal-container') $container = $('#minimal-container')
width = $container.width() width = $container.width()

View File

@ -119,7 +119,7 @@ ConfigureTracksActions = @ConfigureTracksActions
$connectionState, $connectionState,
'SessionStatsHover', 'SessionStatsHover',
() => () =>
{participant: {client_id: this.props.clientId, user: name: 'You', possessive: 'Your'}, } {myTrack: true, participant: {client_id: this.props.connStatsClientId, user: name: 'You', possessive: 'Your'}, }
, ,
{width:385, positions:['right', 'left'], offsetParent:$root.closest('.screen'), extraClasses: 'self'}) {width:385, positions:['right', 'left'], offsetParent:$root.closest('.screen'), extraClasses: 'self'})

View File

@ -9,15 +9,29 @@ MixerActions = context.MixerActions
session = sessionMixers.session session = sessionMixers.session
mixers = sessionMixers.mixers mixers = sessionMixers.mixers
noAudioUsers = mixers.noAudioUsers noAudioUsers = mixers.noAudioUsers
clientsWithAudioOverride = mixers.clientsWithAudioOverride
participants = [] participants = []
if session.inSession() if session.inSession()
self = session.getParticipant(@app.clientId)
myJamBlasterClientId = null
if self? && self.client_role == 'child' && self.parent_client_id?
myJamBlasterClientId = self.parent_client_id
for participant in session.otherParticipants() for participant in session.otherParticipants()
#if participant.is_jamblaster if myJamBlasterClientId? && participant.client_id == myJamBlasterClientId
#continue # don't show my parent jamblaster in 'others'
continue
if participant.client_role == 'child' && participant.parent_client_id?
#participant.parent = session.getParticipant(participant.parent_client_id)
# don't show children nodes
continue
if participant.client_id == @app.clientId if participant.client_id == @app.clientId
participant.user.possessive = "Your" participant.user.possessive = "Your"
@ -55,6 +69,10 @@ MixerActions = context.MixerActions
name = "#{name}: #{instrumentDescription}" name = "#{name}: #{instrumentDescription}"
noAudio = false
if !clientsWithAudioOverride[participant.client_id]
noAudio = noAudioUsers[participant.client_id]
participantState = { participantState = {
participant: participant, participant: participant,
tracks: tracks, tracks: tracks,
@ -63,7 +81,7 @@ MixerActions = context.MixerActions
instrumentIcon: instrumentIcon, instrumentIcon: instrumentIcon,
photoUrl: photoUrl, photoUrl: photoUrl,
hasMixer: hasMixer, hasMixer: hasMixer,
noAudio: noAudioUsers[participant.client_id] noAudio: noAudio
} }
MixerActions.missingPeerMixer(participant.client_id) unless hasMixer MixerActions.missingPeerMixer(participant.client_id) unless hasMixer

View File

@ -81,7 +81,7 @@ MixerActions = @MixerActions
<div>Volume</div> <div>Volume</div>
<div>{monitorVolumeLeft}dB</div> <div>{monitorVolumeLeft}dB</div>
</div> </div>
<SessionTrackGain mixers={this.state.inputGroupMixers} gainType='music' /> <SessionTrackGain mixers={this.state.inputGroupMixers} gainType='music' controlGroup="music" />
<div className={monitorMuteClasses} data-control="mute" onClick={this.handleAudioInputMute}/> <div className={monitorMuteClasses} data-control="mute" onClick={this.handleAudioInputMute}/>
<input type="checkbox" name="mute"/> <input type="checkbox" name="mute"/>
@ -108,7 +108,7 @@ MixerActions = @MixerActions
<div>Volume</div> <div>Volume</div>
<div>{chatVolumeLeft}dB</div> <div>{chatVolumeLeft}dB</div>
</div> </div>
<SessionTrackGain mixers={this.state.chatGroupMixers} /> <SessionTrackGain mixers={this.state.chatGroupMixers} controlGroup="chat" />
<div className={chatMuteClasses} data-control="mute" onClick={this.handleChatMute}/> <div className={chatMuteClasses} data-control="mute" onClick={this.handleChatMute}/>
<input type="checkbox" name="mute"/> <input type="checkbox" name="mute"/>

View File

@ -143,7 +143,7 @@ StatsInfo = {
audio = @state.stats?.audio audio = @state.stats?.audio
aggregateTag = null aggregateTag = null
if aggregate? if aggregate? && !this.props.myTrack
if aggregate.latency if aggregate.latency
aggregateStats.push(@stat(aggregate, 'aggregate', 'Tot Latency', 'latency', Math.round(aggregate.latency))) aggregateStats.push(@stat(aggregate, 'aggregate', 'Tot Latency', 'latency', Math.round(aggregate.latency)))
@ -173,10 +173,16 @@ StatsInfo = {
audio_type = 'WDM' audio_type = 'WDM'
else if audio_long.indexOf('core') > -1 else if audio_long.indexOf('core') > -1
audio_type = 'CoreAudio' audio_type = 'CoreAudio'
else if audio_long.indexOf('alsa') > -1
audio_type = 'JamBlaster'
audioStats.push(@stat(audio, 'audio', 'Gear Driver', 'audio_in_type', audio_type)) audioStats.push(@stat(audio, 'audio', 'Gear Driver', 'audio_in_type', audio_type))
if audio.framesize? if audio.framesize?
framesize = '?' framesize = '?'
if audio.framesize == 2.5 if audio.framesize == 1.0
framesize = '1 ms'
else if audio.framesize == 2.0
framesize = '1 ms'
else if audio.framesize == 2.5
framesize = '2.5 ms' framesize = '2.5 ms'
else if audio.framesize == 5 else if audio.framesize == 5
framesize = '5 ms' framesize = '5 ms'
@ -185,7 +191,7 @@ StatsInfo = {
audioStats.push(@stat(audio, 'audio', 'Frame Size', 'framesize', framesize)) audioStats.push(@stat(audio, 'audio', 'Frame Size', 'framesize', framesize))
networkTag = null networkTag = null
if network? if network? && !this.props.myTrack
if network.ping? if network.ping?
networkStats.push(@stat(network, 'network', 'Latency', 'ping', (network.ping / 2).toFixed(1) + ' ms')) networkStats.push(@stat(network, 'network', 'Latency', 'ping', (network.ping / 2).toFixed(1) + ' ms'))
if network.audiojq_median? if network.audiojq_median?
@ -238,7 +244,11 @@ StatsInfo = {
onStatsChanged: (stats) -> onStatsChanged: (stats) ->
stats = window.SessionStatsStore.stats stats = window.SessionStatsStore.stats
if stats? if stats?
clientStats = stats[@props.participant.client_id] if stats.parent?
# if we have a parent, then use stats from the JamBlaster (parent), not ourselves. Otherwise we'll get bad stats (no Audio etc)
clientStats = stats.parent[@props.participant.client_id]
else
clientStats = stats[@props.participant.client_id]
else else
clientStats = null clientStats = null
@setState({stats: clientStats}) @setState({stats: clientStats})

View File

@ -8,6 +8,7 @@ MIX_MODES = context.JK.MIX_MODES
propTypes: { propTypes: {
gainType: React.PropTypes.string gainType: React.PropTypes.string
controlGroup: React.PropTypes.string
} }
getInitialState: () -> getInitialState: () ->
@ -22,12 +23,11 @@ MIX_MODES = context.JK.MIX_MODES
mixers = @state.mixers.mixer mixers = @state.mixers.mixer
# if this is a media track, jam track , or media category, affect volume of both mixer and opposing mixer # if this is a media track, jam track , or media category, affect volume of both mixer and opposing mixer
if @state.mixers.mixer.group_id == ChannelGroupIds.MediaTrackGroup || @state.mixers.mixer.group_id == ChannelGroupIds.JamTrackGroup || ((@state.mixers.mixer.group_id == ChannelGroupIds.MonitorCatGroup || @state.mixers.mixer.group_id == ChannelGroupIds.MasterCatGroup) && @state.mixers.mixer.name == CategoryGroupIds.MediaTrack) if @state.mixers.mixer.group_id == ChannelGroupIds.MediaTrackGroup || @state.mixers.mixer.group_id == ChannelGroupIds.JamTrackGroup || ((@state.mixers.mixer.group_id == ChannelGroupIds.MonitorCatGroup || @state.mixers.mixer.group_id == ChannelGroupIds.MasterCatGroup) && @state.mixers.mixer.name == CategoryGroupIds.MediaTrack)
MixerActions.faderChanged(data, [@state.mixers.mixer, @state.mixers.oppositeMixer], @props.gainType) MixerActions.faderChanged(data, [@state.mixers.mixer, @state.mixers.oppositeMixer], @props.gainType, @props.controlGroup)
else else
MixerActions.faderChanged(data, mixers, @props.gainType) MixerActions.faderChanged(data, mixers, @props.gainType, @props.controlGroup)
render: () -> render: () ->
# mixer can be a single item or array # mixer can be a single item or array

View File

@ -0,0 +1,106 @@
context = window
MIX_MODES = context.JK.MIX_MODES
@ShoppingCartContents = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
render: () ->
carts = []
if this.props.carts?
if this.props.carts.length == 0
carts = `<div className="no-cart-items">You have nothing in your cart</div>`
else
taxRate = 0
if this.props.tax
taxRate = 0.0825
estimatedTax = 0
estimatedTotal = 0
for cart in this.props.carts
cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem
estimatedTax += cart.product_info.price * cart_quantity * taxRate
estimatedTotal += cart.product_info.price * cart_quantity
estimatedTax = Math.round(estimatedTax * 100) / 100
estimatedTotal = Math.round((estimatedTotal + estimatedTax) * 100) / 100
for cart in this.props.carts
console.log("CART", cart)
freeNotice = null
if cart.product_info.free
freeNotice = `<span className="first-one-free">| (first one free)</span>`
carts.push(`<div className="cart-item" key={cart.id}>
<div className="cart-item-caption">
<span>{cart.product_info.sale_display}</span>
{freeNotice}
</div>
<div className="cart-item-price">
$ {Number(cart.product_info.real_price).toFixed(2)}
</div>
<div className="cart-item-quantity">
{cart.quantity}
</div>
<div className="clearall"/>
</div>`)
carts.push(`<div className="cart-item tax-total" key={'tax'}>
<div className="cart-item-caption">
<span>Tax</span>
</div>
<div className="cart-item-price">
$ {estimatedTax.toFixed(2)}
</div>
<div className="cart-item-quantity">
</div>
<div className="clearall"/>
</div>`)
carts.push(`<div className="cart-item total" key={'total'}>
<div className="cart-item-caption">
<span>Total</span>
</div>
<div className="cart-item-price">
$ {estimatedTotal.toFixed(2)}
</div>
<div className="cart-item-quantity">
</div>
<div className="clearall"/>
</div>`)
else
carts = `<div className="loading-indicator">Loading...</div>`
`<div className="shopping-cart-contents">
<div className="order-items-page">
<div className="cart-items">
<div className="cart-item-caption">
<span>YOUR ORDER INCLUDES:</span>
</div>
<div className="cart-item-price">
<span>PRICE</span>
</div>
<div className="cart-item-quantity">
<span>QUANTITY</span>
</div>
<div className="clearall"></div>
{carts}
<div className="clearall"></div>
</div>
</div>
</div>`
onAppInit: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
onUserChanged: (userState) ->
@user = userState?.user
})

View File

@ -43,6 +43,7 @@ proficiencyDescriptionMap = {
TILE_RATINGS: 'ratings' TILE_RATINGS: 'ratings'
TILE_PRICES: 'prices' TILE_PRICES: 'prices'
visible: false visible: false
profileClipboard: null
TILES: ['about', 'experience', 'samples', 'ratings', 'prices'] TILES: ['about', 'experience', 'samples', 'ratings', 'prices']
@ -65,11 +66,26 @@ proficiencyDescriptionMap = {
@root = $(@getDOMNode()) @root = $(@getDOMNode())
@screen = $('#teacher-profile') @screen = $('#teacher-profile')
@starbox() @starbox()
@clipboard()
componentDidUpdate:() -> componentDidUpdate:() ->
@starbox() @starbox()
context.JK.popExternalLinks(@root) context.JK.popExternalLinks(@root)
@clipboard()
clipboard: () ->
$profileLink = @root.find('.copy-profile-link')
if $profileLink.length > 0 && !@profileClipboard?
# mount it
@profileClipboard = new Clipboard($profileLink.get(0), {
text: =>
return context.JK.makeAbsolute('/client#/teacher/profile/' + @state.user.teacher?.id)
})
else if $profileLink.length == 0 && @profileClipboard?
@profileClipboard.destroy()
@profileClipboard = null
starbox:() -> starbox:() ->
$ratings = @root.find('.ratings-box') $ratings = @root.find('.ratings-box')
@ -230,7 +246,9 @@ proficiencyDescriptionMap = {
biography = biography.replace(/\n/g, "<br/>") biography = biography.replace(/\n/g, "<br/>")
`<div className="section bio"> `<div className="section bio">
<h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3> <a className="copy-profile-link button-orange" href='#' onClick={this.copyProfileLink}>
COPY PROFILE URL TO CLIPBOARD
</a><h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3>
<div className="section-content"> <div className="section-content">
<div dangerouslySetInnerHTML={{__html: biography}}></div> <div dangerouslySetInnerHTML={{__html: biography}}></div>
</div> </div>
@ -694,6 +712,16 @@ proficiencyDescriptionMap = {
</div> </div>
</div>` </div>`
copyProfileLink: (e) ->
e.preventDefault()
@app.layout.notify({
title: 'Teacher Profile Link Copied',
text: "Your clipboard now has a link to this teacher that you can share with anyone."
})
selectionMade: (selection, e) -> selectionMade: (selection, e) ->
e.preventDefault() e.preventDefault()

View File

@ -14,4 +14,5 @@ context = window
metronomeChanged: {} metronomeChanged: {}
deadUserRemove: {} deadUserRemove: {}
missingPeerMixer: {} missingPeerMixer: {}
clientsWithAudio: {}
}) })

View File

@ -11,4 +11,5 @@ context = window
abortedRecording: {} abortedRecording: {}
openRecordingControls: {} openRecordingControls: {}
recordingControlsClosed: {} recordingControlsClosed: {}
mixTransferred: {}
}) })

View File

@ -8,7 +8,7 @@ MIX_MODES = context.JK.MIX_MODES;
@MixerHelper = class MixerHelper @MixerHelper = class MixerHelper
constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixMode) -> constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixMode) ->
@mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions @mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions
@app = @session.app @app = @session.app
@mixersByResourceId = {} @mixersByResourceId = {}
@ -622,13 +622,12 @@ MIX_MODES = context.JK.MIX_MODES;
return mixerPair.personal return mixerPair.personal
findMixerForTrack: (client_id, track, myTrack, mode = MIX_MODES.PERSONAL) -> findMixerForTrack: (client_id, track, myTrack, mode) ->
mixer = null # what is the best mixer for this track/client ID? mixer = null # what is the best mixer for this track/client ID?
oppositeMixer = null # what is the corresponding mixer in the opposite mode? oppositeMixer = null # what is the corresponding mixer in the opposite mode?
vuMixer = null vuMixer = null
muteMixer = null muteMixer = null
if myTrack if myTrack
# when it's your track, look it up by the backend resource ID # when it's your track, look it up by the backend resource ID
mixer = @getMixerByTrackId(track.client_track_id, mode) mixer = @getMixerByTrackId(track.client_track_id, mode)
@ -674,7 +673,7 @@ MIX_MODES = context.JK.MIX_MODES;
oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0] oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0]
if !oppositeMixer if !oppositeMixer
logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, @personalMixers ) logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer, @personalMixers)
when MIX_MODES.PERSONAL when MIX_MODES.PERSONAL
mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL)
@ -693,6 +692,8 @@ MIX_MODES = context.JK.MIX_MODES;
logger.error("personaol: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer) logger.error("personaol: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer)
#vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs
else
logger.error("no UserMusicInputGroup for client_id #{client_id} in PERSONAL mode", mixers)
{ {
mixer: mixer, mixer: mixer,
@ -729,26 +730,43 @@ MIX_MODES = context.JK.MIX_MODES;
originalVolume originalVolume
faderChanged: (data, mixers, gainType) -> faderChanged: (data, mixers, gainType, controlGroup) ->
mixers = [mixers] unless $.isArray(mixers) mixers = [mixers] unless $.isArray(mixers)
originalVolume = @getOriginalVolume(mixers, gainType) originalVolume = @getOriginalVolume(mixers, gainType)
for mixer in mixers if controlGroup?
broadcast = !(data.dragging) # If fader is still dragging, don't broadcast mixers = [mixers[0]]
mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)
relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack) for mixer in mixers
broadcast = !(data.dragging) # If fader is still dragging, don't broadcast
mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)
@setMixerVolume(mixer, data.percentage, relative, originalVolume) relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack)
# keep state of mixer in sync with backend @setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup)
mixer = @getMixer(mixer.id, mixer.mode)
mixer.volume_left = context.trackVolumeObject.volL
#if groupId == ChannelGroupIds.UserMusicInputGroup # keep state of mixer in sync with backend
# # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well mixer = @getMixer(mixer.id, mixer.mode)
# context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage) mixer.volume_left = context.trackVolumeObject.volL
else
for mixer in mixers
broadcast = !(data.dragging) # If fader is still dragging, don't broadcast
mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)
relative = gainType == 'music' && (mixer.name == CategoryGroupIds.UserMedia || mixer.name == CategoryGroupIds.MediaTrack)
@setMixerVolume(mixer, data.percentage, relative, originalVolume)
# keep state of mixer in sync with backend
mixer = @getMixer(mixer.id, mixer.mode)
mixer.volume_left = context.trackVolumeObject.volL
#if groupId == ChannelGroupIds.UserMusicInputGroup
# # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well
# context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage)
initGain: (mixer) -> initGain: (mixer) ->
if $.isArray(mixer) if $.isArray(mixer)
@ -791,7 +809,7 @@ MIX_MODES = context.JK.MIX_MODES;
mixer = @getMixer(mixer.id, mixer.mode) mixer = @getMixer(mixer.id, mixer.mode)
mixer.loop = context.trackVolumeObject.loop mixer.loop = context.trackVolumeObject.loop
setMixerVolume: (mixer, volumePercent, relative, originalVolume) -> setMixerVolume: (mixer, volumePercent, relative, originalVolume, controlGroup) ->
### ###
// The context.trackVolumeObject has been filled with the mixer values // The context.trackVolumeObject has been filled with the mixer values
// that go with mixerId, and the range of that mixer // that go with mixerId, and the range of that mixer
@ -821,7 +839,15 @@ MIX_MODES = context.JK.MIX_MODES;
else else
context.trackVolumeObject.volL = newVolume context.trackVolumeObject.volL = newVolume
context.trackVolumeObject.volR = newVolume context.trackVolumeObject.volR = newVolume
context.jamClient.SessionSetControlState(mixer.id, mixer.mode); if controlGroup?
if mixer.mode == MIX_MODES.PERSONAL
controlGroupsArg = 0
else
controlGroupsArg = 1
context.jamClient.setSessionMixerCategoryPlayoutState(controlGroup == 'music', controlGroupsArg);
else
context.jamClient.SessionSetControlState(mixer.id, mixer.mode);
percentFromMixerValue: (min, max, value) -> percentFromMixerValue: (min, max, value) ->
try try

View File

@ -98,7 +98,7 @@ rest = context.JK.Rest()
<li>And more</li> <li>And more</li>
</ul> </ul>
<p> <p>
JamTracks sell for $1.99 each. Musicians love to play with these, and typically buy a few at a JamTracks sell for $1.99 each ($4.99 to include ability to download). Musicians love to play with these, and typically buy a few at a
time. Imagine that you are selling a set of guitar strings to an electric guitar player. As a time. Imagine that you are selling a set of guitar strings to an electric guitar player. As a

View File

@ -72,7 +72,7 @@ rest = context.JK.Rest()
<a href="/client#/jamtrack/search" onClick={this.redeem} className="cta-free-jamtrack" alt="ClICK HERE TO PICK YOUR FIRST JAMTRACK FREE!"> <a href="/client#/jamtrack/search" onClick={this.redeem} className="cta-free-jamtrack" alt="ClICK HERE TO PICK YOUR FIRST JAMTRACK FREE!">
{img} {img}
</a> </a>
<span className="value-indicator">$1.99 value</span> <span className="value-indicator">#{this.props.jam_track.download_price} value</span>
</div> </div>
<br/> <br/>
<div className="browse-band"> <div className="browse-band">

View File

@ -35,10 +35,10 @@ rest = context.JK.Rest()
if loggedIn if loggedIn
loggedInCtaButton = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})} onClick={this.ctaClick}>{ctaButtonText}</button>` loggedInCtaButton = `<button className={classNames({'cta-button' : true, 'processing': this.state.processing})} onClick={this.ctaClick}>{ctaButtonText}</button>`
if !@isFree() if !@isFree()
loggedInPriceAdvisory = `<div className="price-advisory">${this.props.jam_track.price}</div>` loggedInPriceAdvisory = `<div className="price-advisory">${this.props.jam_track.download_price}</div>`
else else
if !@isFree() if !@isFree()
loggedOutPriceAdvisory = `<div className="price-advisory">${this.props.jam_track.price}</div>` loggedOutPriceAdvisory = `<div className="price-advisory">${this.props.jam_track.download_price}</div>`
if this.state.loginErrors? if this.state.loginErrors?
for key, value of this.state.loginErrors for key, value of this.state.loginErrors

View File

@ -108,7 +108,7 @@ context = window
<iframe src="//www.youtube.com/embed/ysptXwFYDhQ" frameborder="0" allowfullscreen="allowfullscreen"/> <iframe src="//www.youtube.com/embed/ysptXwFYDhQ" frameborder="0" allowfullscreen="allowfullscreen"/>
</div> </div>
</div> </div>
<p>Your first JamTrack is free, and after that JamTracks are just $1.99 each.</p> <p>Your first JamTrack is free, and after that JamTracks are just $1.99/$4.99 each.</p>
<div className="clearall"/> <div className="clearall"/>
</div> </div>
</div> </div>

View File

@ -22,7 +22,7 @@ badCode = 'This is not a valid code. Please carefully re-enter the code and try
if errorText? && errorText.indexOf('already claimed') > -1 if errorText? && errorText.indexOf('already claimed') > -1
errorText = 'This card has already been claimed. If you believe this is in error, please email us at support@jamkazam.com to report this problem.' errorText = 'This card has already been claimed. If you believe this is in error, please email us at support@jamkazam.com to report this problem.'
buttonClassnames = classNames({'redeem-giftcard': true, 'button-orange': true, disabled: @state.processing || @state.done }) buttonClassnames = classNames({'redeem-giftcard': true, 'button-orange': true, disabled: @state.processing || @state.done })

View File

@ -0,0 +1,16 @@
context = window
rest = context.JK.Rest()
@SimpleJamClassPage = React.createClass({
render: () ->
`<div className="container">
<div className="video-wrapper">
<div className="video-container">
<iframe src="//www.youtube.com/embed/6lICn4g5X-Q" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
</div>`
})

View File

@ -0,0 +1,16 @@
context = window
rest = context.JK.Rest()
@SimpleJamTracksPage = React.createClass({
render: () ->
`<div className="container">
<div className="video-wrapper">
<div className="video-container">
<iframe src="//www.youtube.com/embed/-rHfJggbgqk" frameborder="0" allowfullscreen="allowfullscreen"/>
</div>
</div>
</div>`
})

View File

@ -28,6 +28,12 @@ MIDI_TRACK = context.JK.MIDI_TRACK
if session.inSession() if session.inSession()
participant = session.getParticipant(@app.clientId) participant = session.getParticipant(@app.clientId)
connStatsClientId = @app.clientId
if participant.client_role == 'child' && participant.parent_client_id?
participant.parent = session.getParticipant(participant.parent_client_id)
connStatsClientId = participant.parent_client_id
if participant if participant
photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);
@ -79,7 +85,7 @@ MIDI_TRACK = context.JK.MIDI_TRACK
associatedVst = vst associatedVst = vst
break break
tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id, associatedVst: associatedVst}) tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id, associatedVst: associatedVst, connStatsClientId: connStatsClientId})
else else
logger.warn("SessionMyTracks: unable to find participant") logger.warn("SessionMyTracks: unable to find participant")

View File

@ -1,6 +1,7 @@
$ = jQuery $ = jQuery
context = window context = window
logger = context.JK.logger logger = context.JK.logger
RecordingActions = @RecordingActions
SessionActions = @SessionActions SessionActions = @SessionActions
JamBlasterActions = @JamBlasterActions JamBlasterActions = @JamBlasterActions
@ -27,6 +28,9 @@ JamBlasterActions = @JamBlasterActions
JamBlasterActions.pairState(map) JamBlasterActions.pairState(map)
else if map.cmd == 'jamblaster_tracks_updated' else if map.cmd == 'jamblaster_tracks_updated'
JamBlasterActions.jamblasterTracksUpdated() JamBlasterActions.jamblasterTracksUpdated()
else if map.cmd == 'file_xfer_from_parent'
if map.filename && map.filename.indexOf('RT-mix.wav') > -1
RecordingActions.mixTransferred()
} }
) )

View File

@ -81,7 +81,7 @@ SessionStore = context.SessionStore
query.start = next query.start = next
return query return query
c: (lessonSessionId) -> initializeLesson: (lessonSessionId) ->
@lessonSessionId = lessonSessionId @lessonSessionId = lessonSessionId
@channelType = 'lesson' @channelType = 'lesson'

View File

@ -20,6 +20,7 @@ rest = context.JK.Rest()
checkingMissingPeers : {} checkingMissingPeers : {}
missingMixerPeers : {} missingMixerPeers : {}
recheckTimeout : null recheckTimeout : null
clientsWithAudioOverride : {}
init: -> init: ->
# Register with the app store to get @app # Register with the app store to get @app
@ -38,6 +39,7 @@ rest = context.JK.Rest()
this.listenTo(context.MixerActions.metronomeChanged, this.onMetronomeChanged) this.listenTo(context.MixerActions.metronomeChanged, this.onMetronomeChanged)
this.listenTo(context.MixerActions.deadUserRemove, this.onDeadUserRemove) this.listenTo(context.MixerActions.deadUserRemove, this.onDeadUserRemove)
this.listenTo(context.MixerActions.missingPeerMixer, this.onMissingPeerMixer) this.listenTo(context.MixerActions.missingPeerMixer, this.onMissingPeerMixer)
this.listenTo(context.MixerActions.clientsWithAudio, this.onClientsWithAudio)
context.JK.HandleVolumeChangeCallback2 = @handleVolumeChangeCallback context.JK.HandleVolumeChangeCallback2 = @handleVolumeChangeCallback
context.JK.HandleMetronomeCallback2 = @handleMetronomeCallback context.JK.HandleMetronomeCallback2 = @handleMetronomeCallback
@ -76,7 +78,7 @@ rest = context.JK.Rest()
# metroSound = args.sound # metroSound = args.sound
SessionActions.syncWithServer() SessionActions.syncWithServer()
@mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixers?.mixMode || MIX_MODES.PERSONAL)
@issueChange() @issueChange()
@ -132,7 +134,7 @@ rest = context.JK.Rest()
@masterMixers = context.jamClient.SessionGetAllControlState(true); @masterMixers = context.jamClient.SessionGetAllControlState(true);
@personalMixers = context.jamClient.SessionGetAllControlState(false); @personalMixers = context.jamClient.SessionGetAllControlState(false);
@mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixers?.mixMode || MIX_MODES.PERSONAL)
@issueChange() @issueChange()
@ -146,9 +148,9 @@ rest = context.JK.Rest()
# simulate a state change to cause a UI redraw # simulate a state change to cause a UI redraw
@issueChange() @issueChange()
onFaderChanged: (data, mixers, gainType) -> onFaderChanged: (data, mixers, gainType, controlGroup) ->
@mixers.faderChanged(data, mixers, gainType) @mixers.faderChanged(data, mixers, gainType, controlGroup)
@issueChange() @issueChange()
@ -171,7 +173,7 @@ rest = context.JK.Rest()
@metro.sound = sound @metro.sound = sound
context.jamClient.SessionSetMetronome(@metro.tempo, @metro.sound, 1, 0); context.jamClient.SessionSetMetronome(@metro.tempo, @metro.sound, 1, 0);
@mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixers?.mixMode || MIX_MODES.PERSONAL)
@issueChange() @issueChange()
onDeadUserRemove: (clientId) -> onDeadUserRemove: (clientId) ->
@ -193,6 +195,9 @@ rest = context.JK.Rest()
@issueChange() @issueChange()
onClientsWithAudio: (clients) ->
@clientsWithAudioOverride = clients
onMissingPeerMixer: (clientId) -> onMissingPeerMixer: (clientId) ->
missingPeerAttempts = @missingMixerPeers[clientId] missingPeerAttempts = @missingMixerPeers[clientId]
@ -219,7 +224,7 @@ rest = context.JK.Rest()
@masterMixers = context.jamClient.SessionGetAllControlState(true); @masterMixers = context.jamClient.SessionGetAllControlState(true);
@personalMixers = context.jamClient.SessionGetAllControlState(false); @personalMixers = context.jamClient.SessionGetAllControlState(false);
logger.debug("MixerStore: recheckForMixers") logger.debug("MixerStore: recheckForMixers")
@mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixers?.mixMode || MIX_MODES.PERSONAL)
@issueChange() @issueChange()
onInitGain: (mixer) -> onInitGain: (mixer) ->
@ -234,7 +239,7 @@ rest = context.JK.Rest()
logger.debug("MixerStore: onMixersChanged") logger.debug("MixerStore: onMixersChanged")
@mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @clientsWithAudioOverride, @mixers?.mixMode || MIX_MODES.PERSONAL)
SessionActions.mixersChanged.trigger(type, text, @mixers.getTrackInfo()) SessionActions.mixersChanged.trigger(type, text, @mixers.getTrackInfo())

View File

@ -33,10 +33,11 @@ BackendToFrontendFPS = {
this.trigger({isRecording: @recordingModel.isRecording()}) this.trigger({isRecording: @recordingModel.isRecording()})
onStartRecording: (recordVideo, recordChat) -> onStartRecording: (recordVideo, recordChat) ->
frameRate = 0
frameRate = context.jamClient.GetCurrentVideoFrameRate() || 0; if recordVideo
if context.jamClient.GetCurrentVideoFrameRate?
frameRate = BackendToFrontendFPS[frameRate] frameRate = context.jamClient.GetCurrentVideoFrameRate() || 0;
frameRate = BackendToFrontendFPS[frameRate]
NoVideoRecordActive = 0 NoVideoRecordActive = 0
WebCamRecordActive = 1 WebCamRecordActive = 1
@ -49,12 +50,14 @@ BackendToFrontendFPS = {
onStartingRecording: (details) -> onStartingRecording: (details) ->
details.cause = 'starting' details.cause = 'starting'
@mixTransferred = false
this.trigger(details) this.trigger(details)
@popupRecordingControls() unless @recordingWindow? @popupRecordingControls() unless @recordingWindow?
onStartedRecording: (details) -> onStartedRecording: (details) ->
details.cause = 'started' details.cause = 'started'
@mixTransferred = false
this.trigger(details) this.trigger(details)
@popupRecordingControls() unless @recordingWindow? @popupRecordingControls() unless @recordingWindow?
@ -92,6 +95,9 @@ BackendToFrontendFPS = {
logger.debug("recording controls closed") logger.debug("recording controls closed")
@recordingWindow = null @recordingWindow = null
onMixTransferred: () ->
@mixTransferred = true
popupRecordingControls: () -> popupRecordingControls: () ->
logger.debug("poupRecordingControls") logger.debug("poupRecordingControls")
@recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=340') @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=340')

View File

@ -6,6 +6,7 @@ EVENTS = context.JK.EVENTS
MIX_MODES = context.JK.MIX_MODES MIX_MODES = context.JK.MIX_MODES
SessionActions = @SessionActions SessionActions = @SessionActions
MixerActions = @MixerActions
SessionStatThresholds = gon.session_stat_thresholds SessionStatThresholds = gon.session_stat_thresholds
NetworkThresholds = SessionStatThresholds.network NetworkThresholds = SessionStatThresholds.network
@ -16,7 +17,9 @@ AggregateThresholds = SessionStatThresholds.aggregate
@SessionStatsStore = Reflux.createStore( @SessionStatsStore = Reflux.createStore(
{ {
listenables: @SessionStatsActions listenables: @SessionStatsActions
rawStats: null rawStats: null,
parentStats: null
clientsWithAudio: null
init: -> init: ->
# Register with the app store to get @app # Register with the app store to get @app
@ -24,8 +27,9 @@ AggregateThresholds = SessionStatThresholds.aggregate
onAppInit: (@app) -> onAppInit: (@app) ->
onPushStats: (stats) -> onPushStats: (stats, parentStats) ->
@rawStats = stats @rawStats = stats
@parentStats = parentStats
@changed() @changed()
classify: (holder, field, threshold) -> classify: (holder, field, threshold) ->
@ -65,17 +69,15 @@ AggregateThresholds = SessionStatThresholds.aggregate
else else
holder[fieldLevel] = 'good' holder[fieldLevel] = 'good'
classifyStats: (statBag) ->
changed: () -> container = {}
@stats = {}
self = null self = null
for participant in @rawStats for participant in statBag
if participant.id == @app.clientId if participant.id == @app.clientId
self = participant self = participant
break break
for participant in @rawStats for participant in statBag
aggregate = {} aggregate = {}
@ -93,6 +95,9 @@ AggregateThresholds = SessionStatThresholds.aggregate
network = participant.network network = participant.network
if network? if network?
if network.audio_bitrate_rx > 0 && network.audio_bitrate_tx > 0
@clientsWithAudio[participant.id] = true
@classify(network, 'audiojq_median', NetworkThresholds) @classify(network, 'audiojq_median', NetworkThresholds)
@classify(network, 'jitter_var', NetworkThresholds) @classify(network, 'jitter_var', NetworkThresholds)
@classify(network, 'audio_bitrate_rx', NetworkThresholds) @classify(network, 'audio_bitrate_rx', NetworkThresholds)
@ -151,8 +156,23 @@ AggregateThresholds = SessionStatThresholds.aggregate
else else
participant.classification = 'unknown' participant.classification = 'unknown'
@stats[participant.id] = participant container[participant.id] = participant
return container
changed: () ->
@clientsWithAudio = {}
@stats = {}
@stats = @classifyStats(@rawStats)
if @parentStats
@stats.parent = @classifyStats(@parentStats)
else
@stats.parent = null
MixerActions.clientsWithAudio(@clientsWithAudio)
# see if we can reset noAudio
@trigger(@stats) @trigger(@stats)
} }
) )

View File

@ -728,8 +728,19 @@ ConfigureTracksActions = @ConfigureTracksActions
context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
context.jamClient.SessionSetConnectionStatusRefreshRate(1000); context.jamClient.SessionSetConnectionStatusRefreshRate(1000);
clientRole = context.jamClient.getClientParentChildRole();
parentClientId = context.jamClient.getParentClientId();
logger.debug("role when joining session: #{clientRole}, parent client id #{parentClientId}")
#context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen)
if clientRole == 0
clientRole = 'child'
else if clientRole == 1
clientRole = 'parent'
if clientRole == '' || !clientRole
clientRole = null
# subscribe to events from the recording model # subscribe to events from the recording model
@recordingRegistration() @recordingRegistration()
@ -741,6 +752,8 @@ ConfigureTracksActions = @ConfigureTracksActions
as_musician: true, as_musician: true,
tracks: @userTracks, tracks: @userTracks,
session_id: @currentSessionId, session_id: @currentSessionId,
client_role: clientRole,
parent_client_id: parentClientId
audio_latency: context.jamClient.FTUEGetExpectedLatency().latency audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
}) })
.done((response) => .done((response) =>
@ -840,8 +853,11 @@ ConfigureTracksActions = @ConfigureTracksActions
@backendStatsInterval = window.setInterval((() => (@updateBackendStats())), 1000) @backendStatsInterval = window.setInterval((() => (@updateBackendStats())), 1000)
updateBackendStats: () -> updateBackendStats: () ->
connectionStats = window.jamClient.getConnectionDetail('') connectionStats = window.jamClient.getConnectionDetail('', false)
SessionStatsActions.pushStats(connectionStats) parentConnectionStats = window.jamClient.getConnectionDetail('', true)
#console.log("CONNECTION STATES", connectionStats)
#console.log("PARENT STATES", parentConnectionStats)
SessionStatsActions.pushStats(connectionStats, parentConnectionStats)
trackChanges: (header, payload) -> trackChanges: (header, payload) ->
if @currentTrackChanges < payload.track_changes_counter if @currentTrackChanges < payload.track_changes_counter

View File

@ -102,7 +102,7 @@
var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false } var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false }
$self.triggerHandler('startedRecording', details); $self.triggerHandler('startedRecording', details);
currentlyRecording = false; currentlyRecording = false;
context.RecordingActions.startedRecording(details); context.RecordingActions.startedRecording(details);
}) })

View File

@ -28,6 +28,7 @@
var $downloadApplicationLink = null; var $downloadApplicationLink = null;
var $noPurchasesPrompt = null; var $noPurchasesPrompt = null;
var shoppingCartItem = null; var shoppingCartItem = null;
var $startUsingJtPopup = null;
function beforeShow() { function beforeShow() {
@ -156,6 +157,12 @@
} }
}) })
} }
console.log("jamtracks", jamTracks)
$startUsingJtPopup.attr('href', '/popups/jamtrack-player/' + jamTracks[0].id)
$startUsingJtPopup.find('.download-jamkazam').html('Click Here to Start Using <b>' + jamTracks[0].name + '</b>')
context.JK.popExternalLinks($startUsingJtPopup.parent())
} }
} }
@ -246,6 +253,7 @@
$backBtn = $screen.find('.back'); $backBtn = $screen.find('.back');
$downloadApplicationLink = $screen.find('.download-jamkazam-wrapper'); $downloadApplicationLink = $screen.find('.download-jamkazam-wrapper');
$noPurchasesPrompt = $screen.find('.no-purchases-prompt') $noPurchasesPrompt = $screen.find('.no-purchases-prompt')
$startUsingJtPopup = $screen.find('.jt-popup')
if ($screen.length == 0) throw "$screen must be specified"; if ($screen.length == 0) throw "$screen must be specified";

View File

@ -9,7 +9,7 @@
//= require jquery.queryparams //= require jquery.queryparams
//= require jquery.hoverIntent //= require jquery.hoverIntent
//= require jquery.cookie //= require jquery.cookie
//= require jquery.clipboard //= require clipboard
//= require jquery.easydropdown //= require jquery.easydropdown
//= require jquery.carousel-1.1 //= require jquery.carousel-1.1
//= require jquery.mousewheel-3.1.9 //= require jquery.mousewheel-3.1.9

View File

@ -867,6 +867,8 @@
else { else {
var inputSampleRate = 44100; var inputSampleRate = 44100;
} }
// ignore all this; just set it to
inputSampleRate = 'DEVICE_DEFAULT'
logger.debug("applying the sample rate based on input device: " + selectedDeviceInfo.input.id + " (" + inputSampleRate + ")"); logger.debug("applying the sample rate based on input device: " + selectedDeviceInfo.input.id + " (" + inputSampleRate + ")");
sampleRate.selectSampleRate(inputSampleRate); sampleRate.selectSampleRate(inputSampleRate);
context.jamClient.FTUESetPreferredMixerSampleRate(sampleRate.selectedSampleRate()); context.jamClient.FTUESetPreferredMixerSampleRate(sampleRate.selectedSampleRate());

View File

@ -46,6 +46,9 @@
else if(value == 96000) { else if(value == 96000) {
setter = 'PREFER_96' setter = 'PREFER_96'
} }
else if (value == 'DEVICE_DEFAULT') {
setter = 'USE_DEVICE_DEFAULT_SR'
}
console.log("SELECT SAMPLE RATE" + value, setter); console.log("SELECT SAMPLE RATE" + value, setter);
context.JK.dropdown($sampleRate.val(setter).easyDropDown('select', setter.toString(), true)) context.JK.dropdown($sampleRate.val(setter).easyDropDown('select', setter.toString(), true))
} }
@ -63,7 +66,7 @@
} }
function resetValues() { function resetValues() {
$sampleRate.val('PREFER_44').easyDropDown('select', 'PREFER_44', true) $sampleRate.val('USE_DEVICE_DEFAULT_SR').easyDropDown('select', 'USE_DEVICE_DEFAULT_SR', true)
} }

View File

@ -33,6 +33,24 @@
} }
} }
.paypal-region {
text-align: center;
margin:10px auto 0;
/**margin: 10px auto 0;
padding: 10px 10px 5px;
background-color: white;
border-radius: 8px;
border-color: #ccc;
border-style: solid;
border-width: 3px;
width:145px;*/
}
.or-text {
margin: 60px auto 0;
text-align:center;
}
h2 { h2 {
color:white; color:white;
background-color:#4d4d4d; background-color:#4d4d4d;

View File

@ -266,9 +266,19 @@
margin-top: 5px; margin-top: 5px;
} }
.jamtrack-variant-help {
margin-bottom:20px;
}
.jamtrack-add-zone {
margin: 8px 0px;
position:relative;
}
.jamtrack-price { .jamtrack-price {
margin-top: 5px; width:100%;
margin: 0 auto 10px;
font-size: 20px; font-size: 20px;
color:white;
display:block;
&.free { &.free {
margin-top:0; margin-top:0;
@ -282,7 +292,7 @@
} }
.jamtrack-add-cart, .jamtrack-add-cart-disabled { .jamtrack-add-cart, .jamtrack-add-cart-disabled {
margin: 8px 0px; position:relative;
} }
.jamtrack-license { .jamtrack-license {

View File

@ -46,6 +46,9 @@
.scooter { .scooter {
margin-bottom:10px; margin-bottom:10px;
} }
div.retailer-split {
margin-top:10px;
}
.store-header { .store-header {
float: left; float: left;
padding-top: 10px; padding-top: 10px;
@ -280,4 +283,10 @@
font-size:12px; font-size:12px;
margin:0; margin:0;
} }
.split-input {
:after {
content: '%';
}
}
} }

View File

@ -0,0 +1,78 @@
@import "client/common.scss";
[data-react-class="PayPalConfirmationScreen"] {
height: 100%;
overflow: scroll;
.content-body-scroller {
height: calc(100% - 30px) ! important; // 15px top and bottom padding, and 48px used by .controls
padding: 15px 30px;
}
.confirm-header {
color: white;
font-size: 20px;
}
.controls.bottom {
margin-top: 20px;
}
.place-order-btn {
text-align: center;
margin-right:0;
}
.or-holder {
margin-top: 20px;
text-align: center;
}
.cancel-order-btn {
margin-top: 20px;
text-align: center;
}
.shopping-cart-contents {
@include border-box_sizing;
width: 50%;
margin-top:20px;
}
.controls {
@include border-box_sizing;
width:50%;
a {
float:right;
}
}
.loading-indicator {
margin-bottom:20px;
padding-bottom:20px;
}
.sold-notice {
h2 {
font-size:30px;
text-align:center;
}
}
.download-jamkazam {
color:$ColorLink;
border-radius: 4px;
border-style:solid;
border-color:#AAA;
border-width:1px;
padding:10px;
margin-top:20px;
display:inline-block;
}
.download-jamkazam-wrapper, .back-to-browsing {
text-align:center;
display:block;
margin-top:35px;
&.hidden {
display:none;
}
}
}

View File

@ -0,0 +1,64 @@
@import "client/common.scss";
.shopping-cart-contents {
background-color:#262626;
border-width:0 1px 0 0;
border-style:solid;
border-color:#333;
padding:20px 20px 0;
.cart-item-caption {
width: 50%;
text-align: left;
float: left;
margin-bottom: 10px;
@include border_box_sizing;
}
.first-one-free {
font-size: 14px;
font-style: italic;
margin-left: 15px;
}
.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 {
}
.tax-total {
margin-top:10px;
border-width:1px 0 0 0;
border-color:white;
border-style:solid;
padding-top:10px;
}
.cart-item.total {
margin-top:5px;
}
}

View File

@ -199,6 +199,9 @@
position:absolute; position:absolute;
} }
.copy-profile-link {
float:right;
}
.spinner-large { .spinner-large {
width:200px; width:200px;

View File

@ -723,7 +723,7 @@
} }
.easydropdown-wrapper { .easydropdown-wrapper {
width:auto; width:auto;
float:right; float:left;
} }
} }

View File

@ -12,4 +12,6 @@
*= require dialogs/dialog *= require dialogs/dialog
*= require icheck/minimal/minimal *= require icheck/minimal/minimal
*= require landings/posa_activation *= require landings/posa_activation
*= require landings/simple_jamtracks
*= require landings/simple_jamclass
*/ */

Some files were not shown because too many files have changed in this diff Show More