Merge branch 'develop' into feature/jam_track_analytics

This commit is contained in:
Steven Miers 2015-04-13 13:10:43 -05:00
commit 0e0ea46771
110 changed files with 2962 additions and 924 deletions

View File

@ -2,12 +2,9 @@ ActiveAdmin.register_page "Recurly Health" do
menu :parent => 'Misc'
content :title => "Recurly Transaction Totals" do
table_for Sale.check_integrity do
table_for Sale.check_integrity_of_jam_track_sales do
column "Total", :total
column "Unknown", :not_known
column "Successes", :succeeded
column "Failures", :failed
column "Refunds", :refunded
column "Voids", :voided
end
end

View File

@ -273,4 +273,6 @@ drop_position_unique_jam_track.sql
recording_client_metadata.sql
preview_support_mp3.sql
jam_track_duration.sql
sales.sql
sales.sql
show_whats_next_count.sql
recurly_adjustments.sql

View File

@ -0,0 +1,24 @@
ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_uuid VARCHAR(500);
ALTER TABLE sale_line_items ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500);
ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_uuid VARCHAR(500);
ALTER TABLE jam_track_rights ADD COLUMN recurly_adjustment_credit_uuid VARCHAR(500);
ALTER TABLE sales ADD COLUMN recurly_invoice_id VARCHAR(500) UNIQUE;
ALTER TABLE sales ADD COLUMN recurly_invoice_number INTEGER;
ALTER TABLE sales ADD COLUMN recurly_subtotal_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_tax_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_total_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN recurly_currency VARCHAR;
ALTER TABLE sale_line_items ADD COLUMN recurly_tax_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN recurly_total_in_cents INTEGER;
ALTER TABLE sale_line_items ADD COLUMN recurly_currency VARCHAR;
ALTER TABLE sale_line_items ADD COLUMN recurly_discount_in_cents INTEGER;
ALTER TABLE sales ADD COLUMN sale_type VARCHAR NOT NULL;
ALTER TABLE recurly_transaction_web_hooks ALTER COLUMN subscription_id DROP NOT NULL;
CREATE INDEX recurly_transaction_web_hooks_invoice_id_ndx ON recurly_transaction_web_hooks(invoice_id);
ALTER TABLE jam_track_rights DROP COLUMN recurly_subscription_uuid;

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN show_whats_next_count INTEGER NOT NULL DEFAULT 0;

View File

@ -69,6 +69,7 @@ require "jam_ruby/connection_manager"
require "jam_ruby/version"
require "jam_ruby/environment"
require "jam_ruby/init"
require "jam_ruby/app/mailers/admin_mailer"
require "jam_ruby/app/mailers/user_mailer"
require "jam_ruby/app/mailers/invited_user_mailer"
require "jam_ruby/app/mailers/corp_mailer"

View File

@ -0,0 +1,22 @@
module JamRuby
# sends out a boring ale
class AdminMailer < ActionMailer::Base
include SendGrid
DEFAULT_SENDER = "JamKazam <noreply@jamkazam.com>"
default :from => DEFAULT_SENDER
sendgrid_category :use_subject_lines
#sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
sendgrid_unique_args :env => Environment.mode
def alerts(options)
mail(to: APP_CONFIG.email_alerts_alias,
body: options[:body],
content_type: "text/plain",
subject: options[:subject])
end
end
end

View File

@ -39,7 +39,7 @@
<td align="left">
<!-- CALL OUT BOX -->
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #ffcc00;" href="http://www.jamkazam.com/client#/account/profile">here to unsubscribe</a> and update your profile settings.
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a style="color: #ffcc00;" href="http://www.jamkazam.com">JamKazam</a>.&nbsp;&nbsp;Click <a style="color: #ffcc00;" href="http://www.jamkazam.com/unsubscribe/#{@user.unsubscribe_token}">here to unsubscribe</a> and update your profile settings.
</font></p>
</td></tr></table>

View File

@ -5,7 +5,7 @@
<% end %>
<% unless @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/client#/account/profile.
This email was sent to you because you have an account at JamKazam / http://www.jamkazam.com. Visit your profile page to unsubscribe: http://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -614,7 +614,8 @@ module JamRuby
def synchronize_recurly(jam_track)
begin
recurly = RecurlyClient.new
recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
# no longer create JamTrack plans: VRFS-3028
# recurly.create_jam_track_plan(jam_track) unless recurly.find_jam_track_plan(jam_track)
rescue RecurlyClientError => x
finish('recurly_create_plan', x.errors.to_s)
return false

View File

@ -24,11 +24,12 @@ module JamRuby
validates :metronome_open, :inclusion => {:in => [true, false]}
validates :as_musician, :inclusion => {:in => [true, false, nil]}
validates :client_type, :inclusion => {:in => CLIENT_TYPES}
validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
validates_numericality_of :last_jam_audio_latency, greater_than: 0, :allow_nil => true
validate :can_join_music_session, :if => :joining_session?
validate :user_or_latency_tester_present
after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
# this is no longer required with the new no-input profile
#after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
after_create :did_create
after_save :report_add_participant
@ -62,11 +63,11 @@ module JamRuby
def state_message
case self.aasm_state.to_sym
when CONNECT_STATE
'Connected'
when STALE_STATE
'Stale'
'Connected'
when STALE_STATE
'Stale'
else
'Idle'
'Idle'
end
end
@ -85,7 +86,7 @@ module JamRuby
def joining_session?
joining_session
end
def can_join_music_session
# puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed?
@ -183,8 +184,8 @@ module JamRuby
end
def associate_tracks(tracks)
self.tracks.clear()
unless tracks.nil?
self.tracks.clear()
tracks.each do |track|
t = Track.new
t.instrument = Instrument.find(track["instrument_id"])

View File

@ -17,7 +17,7 @@ module JamRuby
:original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genre, :genre_id, :sales_region, :price,
:reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount,
:licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes,
:jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, as: :admin
:jam_track_tap_ins_attributes, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin
validates :name, presence: true, uniqueness: true, length: {maximum: 200}
validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 }
@ -199,5 +199,6 @@ module JamRuby
def right_for_user(user)
jam_track_rights.where("user_id=?", user).first
end
end
end

View File

@ -43,7 +43,7 @@ module JamRuby
session_user_history.music_session_id = music_session_id
session_user_history.user_id = user_id
session_user_history.client_id = client_id
session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|")
session_user_history.instruments = tracks.map {|t| t[:instrument_id]}.join("|") if tracks
session_user_history.save
end

View File

@ -2,19 +2,33 @@ module JamRuby
class RecurlyTransactionWebHook < ActiveRecord::Base
belongs_to :user, class_name: 'JamRuby::User'
belongs_to :sale_line_item, class_name: 'JamRuby::SaleLineItem', foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid', inverse_of: :recurly_transactions
belongs_to :sale, class_name: 'JamRuby::Sale', foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id', inverse_of: :recurly_transactions
validates :recurly_transaction_id, presence: true
validates :subscription_id, presence: true
validates :action, presence: true
validates :status, presence: true
validates :amount_in_cents, numericality: {only_integer: true}
validates :user, presence: true
SUCCESSFUL_PAYMENT = 'payment'
FAILED_PAYMENT = 'failed_payment'
REFUND = 'refund'
VOID = 'void'
def is_credit_type?
transaction_type == REFUND || transaction_type == VOID
end
def is_voided?
transaction_type == VOID
end
def is_refund?
transaction_type == REFUND
end
def self.is_transaction_web_hook?(document)
return false if document.root.nil?
@ -68,8 +82,42 @@ module JamRuby
# now that we have the transaction saved, we also need to delete the jam_track_right if this is a refund, or voided
if transaction.transaction_type == 'refund' || transaction.transaction_type == 'void'
right = JamTrackRight.find_by_recurly_subscription_uuid(transaction.subscription_id)
right.destroy if right
sale = Sale.find_by_recurly_invoice_id(transaction.invoice_id)
if sale && sale.is_jam_track_sale?
if sale.sale_line_items.length == 1
if sale.recurly_total_in_cents == transaction.amount_in_cents
jam_track = sale.sale_line_items[0].product
jam_track_right = jam_track.right_for_user(transaction.user) if jam_track
if jam_track_right
jam_track_right.destroy
AdminMailer.alerts({
subject:"NOTICE: #{transaction.user.email} has had JamTrack: #{jam_track.name} revoked",
body: "A void event came from Recurly for sale with Recurly invoice ID #{sale.recurly_invoice_id}. We deleted their right to the track in our own database as a result."
}).deliver
else
AdminMailer.alerts({
subject:"NOTICE: #{transaction.user.email} got a refund, but unable to find JamTrackRight to delete",
body: "This should just mean the user already has no rights to the JamTrackRight when the refund came in. Not a big deal, but sort of weird..."
}).deliver
end
else
AdminMailer.alerts({
subject:"ACTION REQUIRED: #{transaction.user.email} got a refund it was not for total value of a JamTrack sale",
body: "We received a refund notice for an amount that was not the same as the original sale. So, no action was taken in the database. sale total: #{sale.recurly_total_in_cents}, refund amount: #{transaction.amount_in_cents}"
}).deliver
end
else
AdminMailer.alerts({
subject: "ACTION REQUIRED: #{transaction.user.email} has refund on invoice with multiple JamTracks",
body: "You will have to manually revoke any JamTrackRights in our database for the appropriate JamTracks"
}).deliver
end
end
end
transaction
end

View File

@ -3,30 +3,341 @@ module JamRuby
# a sale is created every time someone tries to buy something
class Sale < ActiveRecord::Base
JAMTRACK_SALE = 'jamtrack'
belongs_to :user, class_name: 'JamRuby::User'
has_many :sale_line_items, class_name: 'JamRuby::SaleLineItem'
validates :order_total, numericality: { only_integer: false }
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale, foreign_key: 'invoice_id', primary_key: 'recurly_invoice_id'
validates :order_total, numericality: {only_integer: false}
validates :user, presence: true
def self.create(user)
@@log = Logging.logger[Sale]
def self.index(user, params = {})
limit = params[:per_page]
limit ||= 20
limit = limit.to_i
query = Sale.limit(limit)
.includes([:recurly_transactions, :sale_line_items])
.where('sales.user_id' => user.id)
.order('sales.created_at DESC')
current_page = params[:page].nil? ? 1 : params[:page].to_i
next_page = current_page + 1
# will_paginate gem
query = query.paginate(:page => current_page, :per_page => limit)
if query.length == 0 # no more results
{ query: query, next_page: nil}
elsif query.length < limit # no more results
{ query: query, next_page: nil}
else
{ query: query, next_page: next_page }
end
end
def state
original_total = self.recurly_total_in_cents
is_voided = false
refund_total = 0
recurly_transactions.each do |transaction|
if transaction.is_voided?
is_voided = true
else
end
if transaction.is_refund?
refund_total = refund_total + transaction.amount_in_cents
end
end
# if refund_total is > 0, then you have a refund.
# if voided is true, then in theory the whole thing has been refunded
{
voided: is_voided,
original_total: original_total,
refund_total: refund_total
}
end
def self.preview_invoice(current_user, shopping_carts)
line_items = {jam_tracks: []}
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
end
jam_track_items = preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
line_items[:jam_tracks] = jam_track_items if jam_track_items
# TODO: process shopping_carts_subscriptions
line_items
end
# place_order will create one or more sales based on the contents of shopping_carts for the current user
# 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)
# 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)
sales = []
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
shopping_carts.each do |shopping_cart|
if shopping_cart.is_jam_track?
shopping_carts_jam_tracks << shopping_cart
else
# XXX: this may have to be revisited when we actually have something other than JamTracks for puchase
shopping_carts_subscriptions << shopping_cart
end
end
jam_track_sale = order_jam_tracks(current_user, shopping_carts_jam_tracks)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
sales
end
def self.preview_invoice_jam_tracks(current_user, shopping_carts_jam_tracks)
### XXX TODO;
# we currently use a fake plan in Recurly to estimate taxes using the Pricing.Attach metod in Recurly.js
# if we were to implement this the right way (ensure adjustments are on the account as necessary), then it would be better (more correct)
# just a pain to implement
end
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
def self.order_jam_tracks(current_user, shopping_carts_jam_tracks)
client = RecurlyClient.new
sale = nil
Sale.transaction do
sale = create_jam_track_sale(current_user)
if sale.valid?
account = client.get_account(current_user)
if account.present?
purge_pending_adjustments(account)
created_adjustments = sale.process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
# now invoice the sale ... almost done
begin
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid
sale.recurly_invoice_number = invoice.invoice_number
# now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
sale.recurly_tax_in_cents = invoice.tax_in_cents
sale.recurly_total_in_cents = invoice.total_in_cents
sale.recurly_currency = invoice.currency
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
end
unless sale.save
raise RecurlyClientError, "Invalid sale (at end)."
end
rescue Recurly::Resource::Invalid => e
# this exception is thrown by invoice! if the invoice is invalid
sale.rollback_adjustments(current_user, created_adjustments)
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
end
else
raise RecurlyClientError, "Could not find account to place order."
end
else
raise RecurlyClientError, "Invalid sale."
end
end
sale
end
def process_jam_tracks(current_user, shopping_carts_jam_tracks, account)
created_adjustments = []
begin
shopping_carts_jam_tracks.each do |shopping_cart|
process_jam_track(current_user, shopping_cart, account, created_adjustments)
end
rescue Recurly::Error, NoMethodError => x
# rollback any adjustments created if error
rollback_adjustments(user, created_adjustments)
raise RecurlyClientError, x.to_s
rescue Exception => e
# rollback any adjustments created if error
rollback_adjustments(user, created_adjustments)
raise e
end
created_adjustments
end
def process_jam_track(current_user, shopping_cart, account, created_adjustments)
recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil
# we do this because of ShoppingCart.remove_jam_track_from_cart; if it occurs, which should be rare, we need fresh shopping cart info
shopping_cart.reload
# get the JamTrack in this shopping cart
jam_track = shopping_cart.cart_product
if jam_track.right_for_user(current_user)
# 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
ShoppingCart.remove_jam_track_from_cart(current_user, shopping_cart)
return
end
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user)
adjustments.each do |adjustment|
# create the adjustment at Recurly (this may not look like it, but it is a REST API)
created_adjustment = account.adjustments.new(adjustment)
created_adjustment.save
# if the adjustment could not be made, bail
raise RecurlyClientError.new(created_adjustment.errors) if created_adjustment.errors.any?
# keep track of adjustments we created for this order, in case we have to roll them back
created_adjustments << created_adjustment
if ShoppingCart.is_product_purchase?(adjustment)
# this was a normal product adjustment, so track it as such
recurly_adjustment_uuid = created_adjustment.uuid
else
# this was a 'credit' adjustment, so track it as such
recurly_adjustment_credit_uuid = created_adjustment.uuid
end
end
# create one sale line item for every jam track
sale_line_item = SaleLineItem.create_from_shopping_cart(self, shopping_cart, nil, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
# if the sale line item is invalid, blow up the transaction
unless sale_line_item.valid?
@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale_line_item.errors.to_s, value: 1})
raise RecurlyClientError.new(sale_line_item.errors)
end
# create a JamTrackRight (this needs to be in a transaction too to make sure we don't make these by accident)
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = shopping_cart.free?
end
# also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if shopping_cart.free?
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_adjustment_uuid != recurly_adjustment_uuid
jam_track_right.recurly_adjustment_uuid = recurly_adjustment_uuid
jam_track_right.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
unless jam_track_right.save
raise RecurlyClientError.new(jam_track_right.errors)
end
end
# delete the shopping cart; it's been dealt with
shopping_cart.destroy if shopping_cart
# blow up the transaction if the JamTrackRight did not get created
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
end
def rollback_adjustments(current_user, adjustments)
begin
adjustments.each { |adjustment| adjustment.destroy }
rescue Exception => e
AdminMailer.alerts({
subject: "ACTION REQUIRED: #{current_user.email} did not have all of his adjustments destroyed in rollback",
body: "go delete any adjustments on the account that don't belong. error: #{e}\n\nAdjustments: #{adjustments.inspect}"
}).deliver
end
end
def self.purge_pending_adjustments(account)
account.adjustments.pending.find_each do |adjustment|
# we only pre-emptively destroy pending adjustments if they appear to be created by the server
adjustment.destroy if ShoppingCart.is_server_pending_adjustment?(adjustment)
end
end
def is_jam_track_sale?
sale_type == JAMTRACK_SALE
end
def self.create_jam_track_sale(user)
sale = Sale.new
sale.user = user
sale.sale_type = JAMTRACK_SALE
sale.order_total = 0
sale.save
sale
end
def self.check_integrity
SaleLineItem.select([:total, :not_known, :succeeded, :failed, :refunded, :voided]).find_by_sql(
"SELECT COUNT(sale_line_items.id) AS total,
COUNT(CASE WHEN transactions.id IS NULL THEN 1 ELSE null END) not_known,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT}' THEN 1 ELSE null END) succeeded,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::FAILED_PAYMENT}' THEN 1 ELSE null END) failed,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::REFUND}' THEN 1 ELSE null END) refunded,
# this checks just jamtrack sales appropriately
def self.check_integrity_of_jam_track_sales
Sale.select([:total, :voided]).find_by_sql(
"SELECT COUNT(sales.id) AS total,
COUNT(CASE WHEN transactions.transaction_type = '#{RecurlyTransactionWebHook::VOID}' THEN 1 ELSE null END) voided
FROM sale_line_items
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON subscription_id = recurly_subscription_uuid")
FROM sales
LEFT OUTER JOIN recurly_transaction_web_hooks as transactions ON invoice_id = sales.recurly_invoice_id
WHERE sale_type = '#{JAMTRACK_SALE}'")
end
end
end

View File

@ -1,14 +1,15 @@
module JamRuby
class SaleLineItem < ActiveRecord::Base
belongs_to :sale, class_name: 'JamRuby::Sale'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
JAMBLASTER = 'JamBlaster'
JAMCLOUD = 'JamCloud'
JAMTRACK = 'JamTrack'
belongs_to :sale, class_name: 'JamRuby::Sale'
belongs_to :jam_track, class_name: 'JamRuby::JamTrack'
belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight'
has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid'
validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]}
validates :unit_price, numericality: {only_integer: false}
validates :quantity, numericality: {only_integer: true}
@ -18,10 +19,50 @@ module JamRuby
validates :recurly_plan_code, presence:true
validates :sale, presence:true
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid)
def product
if product_type == JAMTRACK
JamTrack.find_by_id(product_id)
else
raise 'unsupported product type'
end
end
def product_info
item = product
{ name: product.name } if item
end
def state
voided = false
refunded = false
failed = false
succeeded = false
recurly_transactions.each do |transaction|
if transaction.transaction_type == RecurlyTransactionWebHook::VOID
voided = true
elsif transaction.transaction_type == RecurlyTransactionWebHook::REFUND
refunded = true
elsif transaction.transaction_type == RecurlyTransactionWebHook::FAILED_PAYMENT
failed = true
elsif transaction.transaction_type == RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT
succeeded = true
end
end
{
void: voided,
refund: refunded,
fail: failed,
success: succeeded
}
end
def self.create_from_shopping_cart(sale, shopping_cart, recurly_subscription_uuid, recurly_adjustment_uuid, recurly_adjustment_credit_uuid)
product_info = shopping_cart.product_info
sale.order_total = sale.order_total + product_info[:total_price]
sale.order_total = sale.order_total + product_info[:real_price]
sale_line_item = SaleLineItem.new
sale_line_item.product_type = shopping_cart.cart_type
@ -33,7 +74,9 @@ module JamRuby
sale_line_item.recurly_plan_code = product_info[:plan_code]
sale_line_item.product_id = shopping_cart.cart_id
sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid
sale_line_item.sale = sale
sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid
sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid
sale.sale_line_items << sale_line_item
sale_line_item.save
sale_line_item
end

View File

@ -1,8 +1,19 @@
module JamRuby
class ShoppingCart < ActiveRecord::Base
# just a normal purchase; used on the description field of a recurly adjustment
PURCHASE_NORMAL = 'purchase-normal'
# a free purchase; used on the description field of a recurly adjustment
PURCHASE_FREE = 'purchase-free'
# a techinicality of Recurly; we create a free-credit adjustment to balance out the free purchase adjustment
PURCHASE_FREE_CREDIT = 'purchase-free-credit'
PURCHASE_REASONS = [PURCHASE_NORMAL, PURCHASE_FREE, PURCHASE_FREE_CREDIT]
attr_accessible :quantity, :cart_type, :product_info
validates_uniqueness_of :cart_id, scope: :cart_type
belongs_to :user, :inverse_of => :shopping_carts, :class_name => "JamRuby::User", :foreign_key => "user_id"
validates :cart_id, presence: true
@ -14,14 +25,20 @@ module JamRuby
def product_info
product = self.cart_product
{name: product.name, price: product.price, product_id: cart_id, plan_code: product.plan_code, total_price: total_price(product), quantity: quantity, marked_for_redeem: marked_for_redeem} unless product.nil?
{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} unless product.nil?
end
# multiply quantity by price
def total_price(product)
quantity * product.price
end
# multiply (quantity - redeemable) by price
def total_price(product)
def real_price(product)
(quantity - marked_for_redeem) * product.price
end
def cart_product
self.cart_class_name.classify.constantize.find_by_id(self.cart_id) unless self.cart_class_name.blank?
end
@ -51,6 +68,59 @@ module JamRuby
cart
end
def is_jam_track?
cart_type == JamTrack::PRODUCT_TYPE
end
# returns an array of adjustments for the shopping cart
def create_adjustment_attributes(current_user)
raise "not a jam track" unless is_jam_track?
info = self.product_info
if free?
# create the credit, then the pseudo charge
[
{
accounting_code: PURCHASE_FREE_CREDIT,
currency: 'USD',
unit_amount_in_cents: -(info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name] + " (Credit)",
tax_exempt: true
},
{
accounting_code: PURCHASE_FREE,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
tax_exempt: true
}
]
else
[
{
accounting_code: PURCHASE_NORMAL,
currency: 'USD',
unit_amount_in_cents: (info[:total_price] * 100).to_i,
description: "JamTrack: " + info[:name],
tax_exempt: false
}
]
end
end
def self.is_product_purchase?(adjustment)
(adjustment[:accounting_code].include?(PURCHASE_FREE) || adjustment[:accounting_code].include?(PURCHASE_NORMAL)) && !adjustment[:accounting_code].include?(PURCHASE_FREE_CREDIT)
end
# recurly_adjustment is a Recurly::Adjustment (http://www.rubydoc.info/gems/recurly/Recurly/Adjustment)
# this asks, 'is this a pending adjustment?' AND 'was this adjustment created by the server (vs manually by someone -- we should leave those alone).'
def self.is_server_pending_adjustment?(recurly_adjustment)
recurly_adjustment.state == 'pending' && (recurly_adjustment.accounting_code.include?(PURCHASE_FREE) || recurly_adjustment.accounting_code.include?(PURCHASE_NORMAL) || recurly_adjustment.accounting_code.include?(PURCHASE_FREE_CREDIT))
end
# if the user has a redeemable jam_track still on their account, then also check if any shopping carts have already been marked.
# if no shpping carts have been marked, then mark it redeemable
# should be wrapped in a TRANSACTION
@ -73,20 +143,8 @@ module JamRuby
def self.add_jam_track_to_cart(any_user, jam_track)
cart = nil
ShoppingCart.transaction do
# does this user already have this JamTrack in their cart? If so, don't add it.
duplicate_found = false
any_user.shopping_carts.each do |shopping_cart|
if shopping_cart.cart_type == JamTrack::PRODUCT_TYPE && shopping_cart.cart_id == jam_track.id
duplicate_found = true
return
end
end
unless duplicate_found
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
end
mark_redeem = ShoppingCart.user_has_redeemable_jam_track?(any_user)
cart = ShoppingCart.create(any_user, jam_track, 1, mark_redeem)
end
cart
end

View File

@ -375,6 +375,10 @@ module JamRuby
self.purchased_jam_tracks.count
end
def sales_count
self.sales.count
end
def joined_score
return nil unless has_attribute?(:score)
a = read_attribute(:score)
@ -1533,6 +1537,28 @@ module JamRuby
ShoppingCart.where("user_id=?", self).destroy_all
end
def unsubscribe_token
self.class.create_access_token(self)
end
# Verifier based on our application secret
def self.verifier
ActiveSupport::MessageVerifier.new(APP_CONFIG.secret_token)
end
# Get a user from a token
def self.read_access_token(signature)
uid = self.verifier.verify(signature)
User.find_by_id uid
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
# Class method for token generation
def self.create_access_token(user)
verifier.generate(user.id)
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64

View File

@ -1,6 +1,6 @@
require 'recurly'
module JamRuby
class RecurlyClient
class RecurlyClient
def initialize()
@log = Logging.logger[self]
end
@ -11,37 +11,37 @@ module JamRuby
begin
#puts "Recurly.api_key: #{Recurly.api_key}"
account = Recurly::Account.create(options)
raise RecurlyClientError.new(account.errors) if account.errors.any?
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError.new(account.errors) if account.errors.any?
rescue Recurly::Error, NoMethodError => x
#puts "Error: #{x} : #{Kernel.caller}"
raise RecurlyClientError, x.to_s
else
if account
current_user.update_attribute(:recurly_code, account.account_code)
end
end
account
current_user.update_attribute(:recurly_code, account.account_code)
end
end
account
end
def has_account?(current_user)
account = get_account(current_user)
account = get_account(current_user)
!!account
end
def delete_account(current_user)
account = get_account(current_user)
if (account)
account = get_account(current_user)
if (account)
begin
account.destroy
account.destroy
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
else
else
raise RecurlyClientError, "Could not find account to delete."
end
account
end
def get_account(current_user)
current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil
rescue Recurly::Error => x
@ -51,9 +51,9 @@ module JamRuby
def update_account(current_user, billing_info=nil)
account = get_account(current_user)
if(account.present?)
options = account_hash(current_user, billing_info)
options = account_hash(current_user, billing_info)
begin
account.update_attributes(options)
account.update_attributes(options)
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
@ -61,12 +61,20 @@ module JamRuby
account
end
def payment_history(current_user)
def payment_history(current_user, options ={})
limit = params[:limit]
limit ||= 20
limit = limit.to_i
cursor = options[:cursor]
payments = []
account = get_account(current_user)
if(account.present?)
begin
account.transactions.find_each do |transaction|
account.transaction.paginate(per_page:limit, cursor:cursor).each do |transaction|
# XXX this isn't correct because we create 0 dollar transactions too (for free stuff)
#if transaction.amount_in_cents > 0 # Account creation adds a transaction record
payments << {
@ -74,7 +82,8 @@ module JamRuby
:amount_in_cents => transaction.amount_in_cents,
:status => transaction.status,
:payment_method => transaction.payment_method,
:reference => transaction.reference
:reference => transaction.reference,
:plan_code => transaction.plan_code
}
#end
end
@ -95,7 +104,7 @@ module JamRuby
raise RecurlyClientError, x.to_s
end
raise RecurlyClientError.new(account.errors) if account.errors.any?
raise RecurlyClientError.new(account.errors) if account.errors.any?
else
raise RecurlyClientError, "Could not find account to update billing info."
end
@ -121,21 +130,21 @@ module JamRuby
#puts "subscription.plan.plan_code: #{subscription.plan.plan_code} / #{jam_track.plan_code} / #{subscription.plan.plan_code == jam_track.plan_code}"
if(subscription.plan.plan_code == jam_track.plan_code)
subscription.terminate(:full)
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
terminated = true
end
end
end
if terminated
jam_track_right.destroy()
jam_track_right.destroy()
else
raise RecurlyClientError, "Subscription '#{jam_track.plan_code}' not found for this user; could not issue refund."
end
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
else
raise RecurlyClientError, "Could not find account to refund order."
end
@ -177,89 +186,18 @@ module JamRuby
raise RecurlyClientError.new(plan.errors) if plan.errors.any?
end
def place_order(current_user, jam_track, shopping_cart, sale)
jam_track_right = nil
account = get_account(current_user)
if (account.present?)
begin
# see if we can find existing plan for this plan_code, which should occur for previous-in-time error scenarios
recurly_subscription_uuid = nil
account.subscriptions.find_each do |subscription|
if subscription.plan.plan_code == jam_track.plan_code
recurly_subscription_uuid = subscription.uuid
break
end
end
free = false
# this means we already have a subscription, so don't try to create a new one for the same plan (Recurly would fail this anyway)
unless recurly_subscription_uuid
# if the shopping cart was specified, see if the item should be free
free = shopping_cart.nil? ? false : shopping_cart.free?
# and if it's free, squish the charge to 0.
unit_amount_in_cents = free ? 0 : nil
subscription = Recurly::Subscription.create(:account=>account, :plan_code=>jam_track.plan_code, unit_amount_in_cents: unit_amount_in_cents)
raise RecurlyClientError.new(subscription.errors) if subscription.errors.any?
# add a line item for the sale
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, subscription.uuid)
unless sale_line_item.valid?
@log.error("sale item invalid! #{sale_line_item.errors.inspect}")
puts("sale item invalid! #{sale_line_item.errors.inspect}")
Stats.write('web.recurly.purchase.sale_invalid', {message: sale.errors.to_s, value:1})
end
# delete from shopping cart the subscription
shopping_cart.destroy if shopping_cart
recurly_subscription_uuid = subscription.uuid
end
#raise RecurlyClientError, "Plan code '#{paid_subscription.plan_code}' doesn't match jam track: '#{jam_track.plan_code}'" unless recurly_subscription_uuid
jam_track_right = JamRuby::JamTrackRight.find_or_create_by_user_id_and_jam_track_id(current_user.id, jam_track.id) do |jam_track_right|
jam_track_right.redeemed = free
end
# also if the purchase was a free one, then update the user record to no longer allow redeemed jamtracks
User.where(id: current_user.id).update_all(has_redeemable_jamtrack: false) if free
# this can't go in the block above, as it's here to fix bad subscription UUIDs in an update path
if jam_track_right.recurly_subscription_uuid != recurly_subscription_uuid
jam_track_right.recurly_subscription_uuid = recurly_subscription_uuid
jam_track_right.save
end
raise RecurlyClientError.new("Error creating jam_track_right for jam_track: #{jam_track.id}") if jam_track_right.nil?
raise RecurlyClientError.new(jam_track_right.errors) if jam_track_right.errors.any?
rescue Recurly::Error, NoMethodError => x
raise RecurlyClientError, x.to_s
end
raise RecurlyClientError.new(account.errors) if account.errors.any?
else
raise RecurlyClientError, "Could not find account to place order."
end
jam_track_right
end
def find_or_create_account(current_user, billing_info)
account = get_account(current_user)
if(account.nil?)
account = create_account(current_user, billing_info)
else
update_billing_info(current_user, billing_info)
end
account
end
account
end
private
def account_hash(current_user, billing_info)
options = {
@ -273,7 +211,7 @@ module JamRuby
country: current_user.country
}
}
options[:billing_info] = billing_info if billing_info
options
end
@ -282,11 +220,11 @@ module JamRuby
class RecurlyClientError < Exception
attr_accessor :errors
def initialize(data)
if data.respond_to?('has_key?')
self.errors = data
if data.respond_to?('has_key?')
self.errors = data
else
self.errors = {:message=>data.to_s}
end
end
end # initialize
def to_s

View File

@ -9,6 +9,7 @@ require 'spec_helper'
describe RecurlyTransactionWebHook do
let(:refund_xml) {'<?xml version="1.0" encoding="UTF-8"?>
<successful_refund_notification>
<account>
@ -120,8 +121,15 @@ describe RecurlyTransactionWebHook do
it "deletes jam_track_right when refunded" do
sale = Sale.create_jam_track_sale(@user)
sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb'
sale.recurly_total_in_cents = 216
sale.save!
# create a jam_track right, which should be whacked as soon as we craete the web hook
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_subscription_uuid: '2da71ad97c826a7b784c264ac59c04de')
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'bleh')
shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil)
document = Nokogiri::XML(refund_xml)
@ -131,8 +139,16 @@ describe RecurlyTransactionWebHook do
end
it "deletes jam_track_right when voided" do
sale = Sale.create_jam_track_sale(@user)
sale.recurly_invoice_id = '2da71ad9c657adf9fe618e4f058c78bb'
sale.recurly_total_in_cents = 216
sale.save!
# create a jam_track right, which should be whacked as soon as we craete the web hook
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_subscription_uuid: '2da71ad97c826a7b784c264ac59c04de')
jam_track_right = FactoryGirl.create(:jam_track_right, user: @user, recurly_adjustment_uuid: 'blah')
shopping_cart = ShoppingCart.create(@user, jam_track_right.jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, '2da71ad9c657adf9fe618e4f058c78bb', nil)
document = Nokogiri::XML(void_xml)

View File

@ -0,0 +1,41 @@
require 'spec_helper'
describe SaleLineItem do
let(:user) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
describe "associations" do
it "can find associated recurly transaction web hook" do
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil)
transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
sale_line_item.reload
sale_line_item.recurly_transactions.should eq([transaction])
end
end
describe "state" do
it "success" do
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid', nil, nil)
transaction = FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
sale_line_item.reload
sale_line_item.state.should eq({
void: false,
refund: false,
fail: false,
success: true
})
end
end
end

View File

@ -1,72 +1,329 @@
require 'spec_helper'
describe Sale do
describe "check_integrity" do
let(:user) {FactoryGirl.create(:user)}
let(:user2) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
describe "index" do
it "empty" do
result = Sale.index(user)
result[:query].length.should eq(0)
result[:next].should eq(nil)
end
it "one" do
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
result = Sale.index(user)
result[:query].length.should eq(1)
result[:next].should eq(nil)
end
it "user filtered correctly" do
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
result = Sale.index(user)
result[:query].length.should eq(1)
result[:next].should eq(nil)
sale2 = Sale.create_jam_track_sale(user2)
shopping_cart = ShoppingCart.create(user2, jam_track)
sale_line_item2 = SaleLineItem.create_from_shopping_cart(sale2, shopping_cart, nil, 'some_adjustment_uuid', nil)
result = Sale.index(user)
result[:query].length.should eq(1)
result[:next].should eq(nil)
end
end
describe "place_order" do
let(:user) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
let(:jamtrack) { FactoryGirl.create(:jam_track) }
let(:jam_track_price_in_cents) { (jamtrack.price * 100).to_i }
let(:client) { RecurlyClient.new }
let(:billing_info) {
info = {}
info[:first_name] = user.first_name
info[:last_name] = user.last_name
info[:address1] = 'Test Address 1'
info[:address2] = 'Test Address 2'
info[:city] = user.city
info[:state] = user.state
info[:country] = user.country
info[:zip] = '12345'
info[:number] = '4111-1111-1111-1111'
info[:month] = '08'
info[:year] = '2017'
info[:verification_value] = '111'
info
}
after(:each) do
if user.recurly_code
account = Recurly::Account.find(user.recurly_code)
if account.present?
account.destroy
end
end
end
it "for a free jam track" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
client.find_or_create_account(user, billing_info)
sales = Sale.place_order(user, [shopping_cart])
user.reload
user.sales.length.should eq(1)
sales.should eq(user.sales)
sale = sales[0]
sale.recurly_invoice_id.should_not be_nil
sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents)
sale.recurly_tax_in_cents.should eq(0)
sale.recurly_total_in_cents.should eq(0)
sale.recurly_currency.should eq('USD')
sale.order_total.should eq(0)
sale.sale_line_items.length.should == 1
sale_line_item = sale.sale_line_items[0]
sale_line_item.recurly_tax_in_cents.should eq(0)
sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents)
sale_line_item.recurly_currency.should eq('USD')
sale_line_item.recurly_discount_in_cents.should eq(0)
sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
sale_line_item.unit_price.should eq(jamtrack.price)
sale_line_item.quantity.should eq(1)
sale_line_item.free.should eq(1)
sale_line_item.sales_tax.should be_nil
sale_line_item.shipping_handling.should eq(0)
sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
sale_line_item.product_id.should eq(jamtrack.id)
sale_line_item.recurly_subscription_uuid.should be_nil
sale_line_item.recurly_adjustment_uuid.should_not be_nil
sale_line_item.recurly_adjustment_credit_uuid.should_not be_nil
sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid)
sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid)
# verify subscription is in Recurly
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should_not be_nil
adjustments.should have(2).items
free_purchase= adjustments[0]
free_purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
free_purchase.accounting_code.should eq(ShoppingCart::PURCHASE_FREE)
free_purchase.description.should eq("JamTrack: " + jamtrack.name)
free_purchase.state.should eq('invoiced')
free_purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
free_credit = adjustments[1]
free_credit.unit_amount_in_cents.should eq(-(jamtrack.price * 100).to_i)
free_credit.accounting_code.should eq(ShoppingCart::PURCHASE_FREE_CREDIT)
free_credit.description.should eq("JamTrack: " + jamtrack.name + " (Credit)")
free_credit.state.should eq('invoiced')
free_credit.uuid.should eq(sale_line_item.recurly_adjustment_credit_uuid)
invoices = recurly_account.invoices
invoices.should have(1).items
invoice = invoices[0]
invoice.uuid.should eq(sale.recurly_invoice_id)
invoice.line_items.should have(2).items # should have both adjustments associated
invoice.line_items[0].should eq(free_credit)
invoice.line_items[1].should eq(free_purchase)
invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i)
invoice.total_in_cents.should eq(0)
invoice.state.should eq('collected')
# verify jam_track_rights data
user.jam_track_rights.should_not be_nil
user.jam_track_rights.should have(1).items
user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id)
user.jam_track_rights.last.redeemed.should be_true
user.has_redeemable_jamtrack.should be_false
end
it "for a normally priced jam track" do
user.has_redeemable_jamtrack = false
user.save!
shopping_cart = ShoppingCart.create user, jamtrack, 1, false
client.find_or_create_account(user, billing_info)
sales = Sale.place_order(user, [shopping_cart])
user.reload
user.sales.length.should eq(1)
sales.should eq(user.sales)
sale = sales[0]
sale.recurly_invoice_id.should_not be_nil
sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents)
sale.recurly_tax_in_cents.should eq(0)
sale.recurly_total_in_cents.should eq(jam_track_price_in_cents)
sale.recurly_currency.should eq('USD')
sale.order_total.should eq(jamtrack.price)
sale.sale_line_items.length.should == 1
sale_line_item = sale.sale_line_items[0]
# validate we are storing pricing info from recurly
sale_line_item.recurly_tax_in_cents.should eq(0)
sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents)
sale_line_item.recurly_currency.should eq('USD')
sale_line_item.recurly_discount_in_cents.should eq(0)
sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
sale_line_item.unit_price.should eq(jamtrack.price)
sale_line_item.quantity.should eq(1)
sale_line_item.free.should eq(0)
sale_line_item.sales_tax.should be_nil
sale_line_item.shipping_handling.should eq(0)
sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code)
sale_line_item.product_id.should eq(jamtrack.id)
sale_line_item.recurly_subscription_uuid.should be_nil
sale_line_item.recurly_adjustment_uuid.should_not be_nil
sale_line_item.recurly_adjustment_credit_uuid.should be_nil
sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid)
# verify subscription is in Recurly
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should_not be_nil
adjustments.should have(1).items
purchase= adjustments[0]
purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i)
purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL)
purchase.description.should eq("JamTrack: " + jamtrack.name)
purchase.state.should eq('invoiced')
purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid)
invoices = recurly_account.invoices
invoices.should have(1).items
invoice = invoices[0]
invoice.uuid.should eq(sale.recurly_invoice_id)
invoice.line_items.should have(1).items # should have single adjustment associated
invoice.line_items[0].should eq(purchase)
invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i)
invoice.total_in_cents.should eq((jamtrack.price * 100).to_i)
invoice.state.should eq('collected')
# verify jam_track_rights data
user.jam_track_rights.should_not be_nil
user.jam_track_rights.should have(1).items
user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id)
user.jam_track_rights.last.redeemed.should be_false
user.has_redeemable_jamtrack.should be_false
end
it "for a jamtrack already owned" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
client.find_or_create_account(user, billing_info)
sales = Sale.place_order(user, [shopping_cart])
user.reload
user.sales.length.should eq(1)
shopping_cart = ShoppingCart.create user, jamtrack, 1, false
sales = Sale.place_order(user, [shopping_cart])
sales.should have(0).items
# also, verify that no earlier adjustments were affected
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should have(2).items
end
# this test counts on the fact that two adjustments are made when buying a free JamTrack
# so if we make the second adjustment invalid from Recurly's standpoint, then
# we can see if the first one is ultimately destroyed
it "rolls back created adjustments if error" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
# grab the real response; we will modify it to make a nil accounting code
adjustment_attrs = shopping_cart.create_adjustment_attributes(user)
client.find_or_create_account(user, billing_info)
adjustment_attrs[1][:unit_amount_in_cents] = nil # invalid amount
ShoppingCart.any_instance.stub(:create_adjustment_attributes).and_return(adjustment_attrs)
expect { Sale.place_order(user, [shopping_cart]) }.to raise_error(JamRuby::RecurlyClientError)
user.reload
user.sales.should have(0).items
recurly_account = client.get_account(user)
recurly_account.adjustments.should have(0).items
end
it "rolls back adjustments created before the order" do
shopping_cart = ShoppingCart.create user, jamtrack, 1, true
client.find_or_create_account(user, billing_info)
# create a single adjustment on the account
adjustment_attrs = shopping_cart.create_adjustment_attributes(user)
recurly_account = client.get_account(user)
adjustment = recurly_account.adjustments.new (adjustment_attrs[0])
adjustment.save
adjustment.errors.any?.should be_false
sales = Sale.place_order(user, [shopping_cart])
user.reload
recurly_account = client.get_account(user)
adjustments = recurly_account.adjustments
adjustments.should have(2).items # two adjustments are created for a free jamtrack; that should be all there is
end
end
describe "check_integrity_of_jam_track_sales" do
let(:user) { FactoryGirl.create(:user) }
let(:jam_track) { FactoryGirl.create(:jam_track) }
it "empty" do
check_integrity = Sale.check_integrity
check_integrity = Sale.check_integrity_of_jam_track_sales
check_integrity.length.should eq(1)
r = check_integrity[0]
r.total.to_i.should eq(0)
r.not_known.to_i.should eq(0)
r.succeeded.to_i.should eq(0)
r.failed.to_i.should eq(0)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
end
it "one unknown sale" do
sale = Sale.create(user)
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid')
check_integrity = Sale.check_integrity
r = check_integrity[0]
r.total.to_i.should eq(1)
r.not_known.to_i.should eq(1)
r.succeeded.to_i.should eq(0)
r.failed.to_i.should eq(0)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
end
it "one succeeded sale" do
sale = Sale.create(user)
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid')
FactoryGirl.create(:recurly_transaction_web_hook, subscription_id: 'some_recurly_uuid')
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil)
check_integrity = Sale.check_integrity
check_integrity = Sale.check_integrity_of_jam_track_sales
r = check_integrity[0]
r.total.to_i.should eq(1)
r.not_known.to_i.should eq(0)
r.succeeded.to_i.should eq(1)
r.failed.to_i.should eq(0)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
end
it "one failed sale" do
sale = Sale.create(user)
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, 'some_recurly_uuid')
FactoryGirl.create(:recurly_transaction_web_hook_failed, subscription_id: 'some_recurly_uuid')
check_integrity = Sale.check_integrity
it "one voided sale" do
sale = Sale.create_jam_track_sale(user)
sale.recurly_invoice_id = 'some_recurly_invoice_id'
sale.save!
shopping_cart = ShoppingCart.create(user, jam_track)
SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_recurly_invoice_id', nil)
FactoryGirl.create(:recurly_transaction_web_hook, transaction_type: RecurlyTransactionWebHook::VOID, invoice_id: 'some_recurly_invoice_id')
check_integrity = Sale.check_integrity_of_jam_track_sales
r = check_integrity[0]
r.total.to_i.should eq(1)
r.not_known.to_i.should eq(0)
r.succeeded.to_i.should eq(0)
r.failed.to_i.should eq(1)
r.refunded.to_i.should eq(0)
r.voided.to_i.should eq(0)
r.voided.to_i.should eq(1)
end
end
end

View File

@ -24,9 +24,10 @@ describe ShoppingCart do
it "should not add duplicate JamTrack to ShoppingCart" do
cart1 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
cart1.should_not be_nil
cart1.errors.any?.should be_false
user.reload
cart2 = ShoppingCart.add_jam_track_to_cart(user, jam_track)
cart2.should be_nil
cart2.errors.any?.should be_true
end

View File

@ -86,42 +86,7 @@ describe RecurlyClient do
found.state.should eq('closed')
end
it "can place order" do
sale = Sale.create(@user)
sale = Sale.find(sale.id)
shopping_cart = ShoppingCart.create @user, @jamtrack, 1, true
history_items = @client.payment_history(@user).length
@client.find_or_create_account(@user, @billing_info)
expect{@client.place_order(@user, @jamtrack, shopping_cart, sale)}.not_to raise_error()
# verify jam_track_rights data
@user.jam_track_rights.should_not be_nil
@user.jam_track_rights.should have(1).items
@user.jam_track_rights.last.jam_track.id.should eq(@jamtrack.id)
# verify sales data
sale = Sale.find(sale.id)
sale.sale_line_items.length.should == 1
sale_line_item = sale.sale_line_items[0]
sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE)
sale_line_item.unit_price.should eq(@jamtrack.price)
sale_line_item.quantity.should eq(1)
sale_line_item.free.should eq(1)
sale_line_item.sales_tax.should be_nil
sale_line_item.shipping_handling.should eq(0)
sale_line_item.recurly_plan_code.should eq(@jamtrack.plan_code)
sale_line_item.product_id.should eq(@jamtrack.id)
sale_line_item.recurly_subscription_uuid.should_not be_nil
sale_line_item.recurly_subscription_uuid.should eq(@user.jam_track_rights.last.recurly_subscription_uuid)
# verify subscription is in Recurly
subs = @client.get_account(@user).subscriptions
subs.should_not be_nil
subs.should have(1).items
@client.payment_history(@user).should have(history_items+1).items
end
=begin
it "can refund subscription" do
sale = Sale.create(@user)
shopping_cart = ShoppingCart.create @user, @jamtrack, 1
@ -141,18 +106,7 @@ describe RecurlyClient do
@jamtrack.reload
@jamtrack.jam_track_rights.should have(0).items
end
=end
it "detects error on double order" do
sale = Sale.create(@user)
shopping_cart = ShoppingCart.create @user, @jamtrack, 1
@client.find_or_create_account(@user, @billing_info)
jam_track_right = @client.place_order(@user, @jamtrack, shopping_cart, sale)
jam_track_right.recurly_subscription_uuid.should_not be_nil
shopping_cart = ShoppingCart.create @user, @jamtrack, 1
jam_track_right2 = @client.place_order(@user, @jamtrack, shopping_cart, sale)
jam_track_right.should eq(jam_track_right2)
jam_track_right.recurly_subscription_uuid.should eq(jam_track_right.recurly_subscription_uuid)
end
end

View File

@ -12,6 +12,7 @@ describe UserMailer do
let(:user) { FactoryGirl.create(:user) }
before(:each) do
stub_const("APP_CONFIG", app_config)
UserMailer.deliveries.clear
end

View File

@ -3,6 +3,14 @@ JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws
def app_config
klass = Class.new do
def email_alerts_alias
'alerts@jamkazam.com'
end
def email_generic_from
'nobody@jamkazam.com'
end
def aws_bucket
JAMKAZAM_TESTING_BUCKET
end
@ -170,6 +178,10 @@ def app_config
true
end
def secret_token
'foobar'
end
private
@ -240,4 +252,4 @@ end
def friend(user1, user2)
FactoryGirl.create(:friendship, user: user1, friend: user2)
FactoryGirl.create(:friendship, user: user2, friend: user1)
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -55,7 +55,8 @@
validProfiles : validProfiles,
invalidProfiles : invalidProfiles,
isNativeClient: gon.isNativeClient,
musician: context.JK.currentUserMusician
musician: context.JK.currentUserMusician,
sales_count: userDetail.sales_count
} , { variable: 'data' }));
$('#account-content-scroller').html($template);
@ -113,7 +114,7 @@
// License dialog:
$("#account-content-scroller").on('click', '#account-view-license-link', function(evt) {evt.stopPropagation(); app.layout.showDialog('jamtrack-license-dialog'); return false; } );
$("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); app.layout.showDialog('jamtrack-payment-history-dialog'); return false; } );
$("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } );
}
function renderAccount() {
@ -157,6 +158,10 @@
window.location = "/client#/account/audio"
}
function navToPaymentHistory() {
window.location = '/client#/account/paymentHistory'
}
// handle update avatar event
function updateAvatar(avatar_url) {
var photoUrl = context.JK.resolveAvatarUrl(avatar_url);

View File

@ -0,0 +1,180 @@
$ = jQuery
context = window
context.JK ||= {}
context.JK.AccountPaymentHistoryScreen = class AccountPaymentHistoryScreen
LIMIT = 20
constructor: (@app) ->
@logger = context.JK.logger
@rest = context.JK.Rest()
@screen = null
@scroller = null
@genre = null
@artist = null
@instrument = null
@availability = null
@nextPager = null
@noMoreSales = null
@currentPage = 0
@next = null
@tbody = null
@rowTemplate = null
beforeShow:(data) =>
afterShow:(data) =>
@refresh()
events:() =>
@backBtn.on('click', @onBack)
onBack:() =>
window.location = '/client#/account'
return false
clearResults:() =>
@currentPage = 0
@tbody.empty()
@noMoreSales.hide()
@next = null
refresh:() =>
@currentQuery = this.buildQuery()
@rest.getSalesHistory(@currentQuery)
.done(@salesHistoryDone)
.fail(@salesHistoryFail)
renderPayments:(response) =>
if response.entries? && response.entries.length > 0
for sale in response.entries
amt = sale.recurly_total_in_cents
amt = 0 if !amt?
original_total = sale.state.original_total
refund_total = sale.state.refund_total
refund_state = null
if original_total != 0 # the enclosed logic does not work for free purchases
if refund_total == original_total
refund_state = 'refunded'
else if refund_total != 0 and refund_total < original_total
refund_state = 'partial refund'
displayAmount = (amt/100).toFixed(2)
status = 'paid'
if sale.state.voided
status = 'voided'
displayAmount = (0).toFixed(2)
else if refund_state?
status = refund_state
displayAmount = (amt/100).toFixed(2) + " (refunded: #{(refund_total/100).toFixed(2)})"
description = []
for line_item in sale.line_items
description.push(line_item.product_info?.name)
payment = {
date: context.JK.formatDate(sale.created_at, true)
amount: displayAmount
status: status
payment_method: 'Credit Card',
description: description.join(', ')
}
tr = $(context._.template(@rowTemplate, payment, { variable: 'data' }));
@tbody.append(tr);
else
tr = "<tr><td class='center' colspan='5'>No payments found</td></tr>"
@tbody.append(tr);
salesHistoryDone:(response) =>
# Turn in to HTML rows and append:
#@tbody.html("")
console.log("response.next", response)
@next = response.next_page
@renderPayments(response)
if response.next_page == null
# if we less results than asked for, end searching
@scroller.infinitescroll 'pause'
@logger.debug("end of history")
if @currentPage > 0
@noMoreSales.show()
# there are bugs with infinitescroll not removing the 'loading'.
# it's most noticeable at the end of the list, so whack all such entries
$('.infinite-scroll-loader').remove()
else
@currentPage++
this.buildQuery()
this.registerInfiniteScroll()
salesHistoryFail:(jqXHR)=>
@noMoreSales.show()
@app.notifyServerError jqXHR, 'Payment History Unavailable'
defaultQuery:() =>
query =
per_page: LIMIT
page: @currentPage+1
if @next
query.since = @next
query
buildQuery:() =>
@currentQuery = this.defaultQuery()
registerInfiniteScroll:() =>
that = this
@scroller.infinitescroll {
behavior: 'local'
navSelector: '#account-payment-history .btn-next-pager'
nextSelector: '#account-payment-history .btn-next-pager'
binder: @scroller
dataType: 'json'
appendCallback: false
prefill: false
bufferPx: 100
loading:
msg: $('<div class="infinite-scroll-loader">Loading ...</div>')
img: '/assets/shared/spinner.gif'
path: (page) =>
'/api/sales?' + $.param(that.buildQuery())
}, (json, opts) =>
this.salesHistoryDone(json)
@scroller.infinitescroll 'resume'
initialize:() =>
screenBindings =
'beforeShow': this.beforeShow
'afterShow': this.afterShow
@app.bindScreen 'account/paymentHistory', screenBindings
@screen = $('#account-payment-history')
@scroller = @screen.find('.content-body-scroller')
@nextPager = @screen.find('a.btn-next-pager')
@noMoreSales = @screen.find('.end-of-payments-list')
@tbody = @screen.find("table.payment-table tbody")
@rowTemplate = $('#template-payment-history-row').html()
@backBtn = @screen.find('.back')
if @screen.length == 0
throw new Error('@screen must be specified')
if @scroller.length == 0
throw new Error('@scroller must be specified')
if @tbody.length == 0
throw new Error('@tbody must be specified')
if @noMoreSales.length == 0
throw new Error('@noMoreSales must be specified')
this.events()

View File

@ -99,7 +99,7 @@
var sub_total = 0.0
var taxes = 0.0
$.each(carts, function(index, cart) {
sub_total += parseFloat(cart.product_info.total_price)
sub_total += parseFloat(cart.product_info.real_price)
});
if(carts.length == 0) {
data.grand_total = '-.--'
@ -147,68 +147,45 @@
var planPricing = {}
var priceElement = $screen.find('.order-right-page .plan.jamtrack')
if(priceElement.length == 0) {
logger.error("unable to find price element for jamtrack");
app.notify({title: "Error Encountered", text: "Unable to find plan info for jam track"})
return false;
}
logger.debug("creating recurly pricing element for plan: " + gon.recurly_tax_estimate_jam_track_plan)
var effectiveQuantity = 0
context._.each(carts, function(cart) {
var priceElement = $screen.find('.order-right-page .plan[data-plan-code="' + cart.product_info.plan_code +'"]')
if(priceElement.length == 0) {
logger.error("unable to find price element for " + cart.product_info.plan_code, cart);
app.notify({title: "Error Encountered", text: "Unable to find plan info for " + cart.product_info.plan_code})
return false;
}
logger.debug("creating recurly pricing element for plan: " + cart.product_info.plan_code)
var pricing = context.recurly.Pricing();
pricing.plan_code = cart.product_info.plan_code;
pricing.resolved = false;
pricing.effective_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem
planPricing[pricing.plan_code] = pricing;
// this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it.
pricing.on('change', function(price) {
var resolvedPrice = planPricing[this.plan_code];
if(!resolvedPrice) {
logger.error("unable to find price info in storage")
app.notify({title: "Error Encountered", text: "Unable to find plan info in storage"})
return;
}
else {
logger.debug("pricing resolved for plan: " + this.plan_code)
}
resolvedPrice.resolved = true;
var allResolved = true;
var totalTax = 0;
var totalPrice = 0;
// let's see if all plans have been resolved via API; and add up total price and taxes for display
$.each(planPricing, function(plan_code, priceObject) {
logger.debug("resolved recurly priceObject", priceObject)
if(!priceObject.resolved) {
allResolved = false;
return false;
}
else {
var unitTax = Number(priceObject.price.now.tax) * priceObject.effective_quantity;
totalTax += unitTax;
var totalUnitPrice = Number(priceObject.price.now.total) * priceObject.effective_quantity;
totalPrice += totalUnitPrice;
}
})
if(allResolved) {
$screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2))
$screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2))
}
else
{
logger.debug("still waiting on more plans to resolve")
}
})
pricing.attach(priceElement.eq(0))
effectiveQuantity += cart.product_info.quantity - cart.product_info.marked_for_redeem
})
var pricing = context.recurly.Pricing();
pricing.plan_code = gon.recurly_tax_estimate_jam_track_plan;
pricing.resolved = false;
pricing.effective_quantity = 1
// this is called when the plan is resolved against Recurly. It will have tax info, which is the only way we can get it.
pricing.on('change', function(price) {
var totalTax = 0;
var totalPrice = 0;
var unitTax = Number(pricing.price.now.tax) * effectiveQuantity;
totalTax += unitTax;
var totalUnitPrice = Number(pricing.price.now.total) * effectiveQuantity;
totalPrice += totalUnitPrice;
$screen.find('.order-right-page .order-items-value.taxes').text('$' + totalTax.toFixed(2))
$screen.find('.order-right-page .order-items-value.grand-total').text('$' + totalPrice.toFixed(2))
})
pricing.attach(priceElement.eq(0))
}
}

View File

@ -0,0 +1,18 @@
# one time init stuff for the /client view
$ = jQuery
context = window
context.JK ||= {};
context.JK.ClientInit = class ClientInit
constructor: () ->
@logger = context.JK.logger
@gearUtils = context.JK.GearUtils
init: () =>
if context.gon.isNativeClient
this.nativeClientInit()
nativeClientInit: () =>
@gearUtils.bootstrapDefaultPlaybackProfile();

View File

@ -9,6 +9,9 @@
var $dialog = null;
var $dontShowAgain = null;
var $setupGearBtn = null;
var $browserJamTrackBtn = null;
var $jamTrackSection = null;
var $jamTracksLimitedTime = null;
function handleStartAudioQualification() {
@ -45,6 +48,12 @@
return false;
})
$browserJamTrackBtn.click(function() {
app.layout.closeDialog('getting-started')
window.location = '/client#/jamtrack'
return false;
})
$('#getting-started-dialog a.facebook-invite').on('click', function (e) {
invitationDialog.showFacebookDialog(e);
});
@ -59,13 +68,21 @@
}
function beforeShow() {
app.user().done(function(user) {
var jamtrackRule = user.free_jamtrack ? 'has-free-jamtrack' : 'no-free-jamtrack'
$jamTrackSection.removeClass('has-free-jamtrack').removeClass('no-free-jamtrack').addClass(jamtrackRule)
if(user.free_jamtrack) {
$jamTracksLimitedTime.removeClass('hidden')
}
})
}
function beforeHide() {
var showWhatsNext = !$dontShowAgain.is(':checked')
app.user().done(function(user) {
app.updateUserModel({show_whats_next: showWhatsNext, show_whats_next_count: user.show_whats_next_count + 1})
})
if ($dontShowAgain.is(':checked')) {
app.updateUserModel({show_whats_next: false})
}
}
function initializeButtons() {
@ -84,6 +101,9 @@
$dialog = $('#getting-started-dialog');
$dontShowAgain = $dialog.find('#show_getting_started');
$setupGearBtn = $dialog.find('.setup-gear-btn')
$browserJamTrackBtn = $dialog.find('.browse-jamtrack');
$jamTrackSection = $dialog.find('.get-a-free-jamtrack-section')
$jamTracksLimitedTime = $dialog.find('.jamtracks-limited-time')
registerEvents();

View File

@ -3,6 +3,7 @@
context.JK = context.JK || {};
context.JK.SessionSettingsDialog = function(app, sessionScreen) {
var logger = context.JK.logger;
var gearUtils = context.JK.GearUtilsInstance;
var $dialog;
var $screen = $('#session-settings');
var $selectedFilenames = $screen.find('#selected-filenames');
@ -15,6 +16,8 @@
function beforeShow(data) {
var canPlayWithOthers = gearUtils.canPlayWithOthers();
context.JK.GenreSelectorHelper.render('#session-settings-genre');
$dialog = $('[layout-id="session-settings"]');
@ -72,6 +75,10 @@
context.JK.dropdown($('#session-settings-language'));
context.JK.dropdown($('#session-settings-musician-access'));
context.JK.dropdown($('#session-settings-fan-access'));
var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable'
$('#session-settings-musician-access').easyDropDown(easyDropDownState)
$('#session-settings-fan-access').easyDropDown(easyDropDownState)
}
function saveSettings(evt) {

View File

@ -0,0 +1,57 @@
$ = jQuery
context = window
context.JK ||= {}
context.JK.SinglePlayerProfileGuardDialog = class SinglePlayerProfileGuardDialog
constructor: (@app) ->
@rest = context.JK.Rest()
@client = context.jamClient
@logger = context.JK.logger
@gearUtils = context.JK.GearUtilsInstance
@screen = null
@dialogId = 'single-player-profile-dialog';
@dialog = null;
initialize:() =>
dialogBindings = {
'beforeShow' : @beforeShow,
'afterShow' : @afterShow
}
@dialog = $('[layout-id="' + @dialogId + '"]');
@app.bindDialog(@dialogId, dialogBindings);
@content = @dialog.find(".dialog-inner")
@audioLatency = @dialog.find('.audio-latency')
@btnPrivateSession = @dialog.find('.btn-private-session')
@btnGearSetup = @dialog.find('.btn-gear-setup')
@btnPrivateSession.on('click', @onPrivateSessionChoice)
@btnGearSetup.on('click', @onGearSetupChoice)
beforeShow:() =>
@dialog.data('result', { choice: null})
afterShow:() =>
canPlayWithOthers = @gearUtils.canPlayWithOthers()
if canPlayWithOthers.isNoInputProfile
@content.removeClass('high-latency').addClass('has-no-inputs')
else
@content.removeClass('has-no-input').addClass('high-latency')
latency = '?'
if canPlayWithOthers.audioLatency?
latency = canPlayWithOthers.audioLatency
@audioLatency.text("#{latency} milliseconds.")
onPrivateSessionChoice: () =>
@dialog.data('result', { choice: 'private_session'})
@app.layout.closeDialog(@dialogId)
return false
onGearSetupChoice: () =>
@dialog.data('result', { choice: 'gear_setup'})
@app.layout.closeDialog(@dialogId)
return false

View File

@ -14,7 +14,7 @@
if (!context.jamClient || !context.jamClient.IsNativeClient()) {
$('#video-dialog-header').html($self.data('video-header') || $self.attr('data-video-header'));
$('#video-dialog-iframe').attr('src', $self.data('video-url') || $self.atr('data-video-url'));
$('#video-dialog-iframe').attr('src', $self.data('video-url') || $self.attr('data-video-url'));
app.layout.showDialog('video-dialog');
e.stopPropagation();
e.preventDefault();
@ -29,6 +29,7 @@
function events() {
$('.carousel .slides').on('click', '.slideItem', videoClick);
$('.video-slide').on('click', videoClick);
$('.video-item').on('click', videoClick);
$(dialogId + '-close').click(function (e) {
app.layout.closeDialog('video-dialog');

View File

@ -204,8 +204,7 @@
var user = app.user()
if(user) {
user.done(function(userProfile) {
console.log("app.layout.getCurrentScreen() != 'checkoutOrderScreen'", app.layout.getCurrentScreen())
if (userProfile.show_whats_next &&
if (userProfile.show_whats_next && userProfile.show_whats_next_count < 10 &&
window.location.pathname.indexOf(gon.client_path) == 0 &&
window.location.pathname.indexOf('/checkout') == -1 &&
!app.layout.isDialogShowing('getting-started'))

View File

@ -25,8 +25,9 @@
var metronomeBPM=false;
var metronomeSound=false;
var metronomeMeter=0;
var backingTrackPath="";
var backingTrackLoop=false;
var backingTrackPath = "";
var backingTrackLoop = false;
var simulateNoInputs = false;
function dbg(msg) { logger.debug('FakeJamClient: ' + msg); }
@ -47,7 +48,13 @@
function FTUEPageLeave() {}
function FTUECancel() {}
function FTUEGetMusicProfileName() {
return "FTUEAttempt-1"
if(simulateNoInputs) {
return "System Default (Playback Only)"
}
else {
return "FTUEAttempt-1"
}
}
function FTUESetMusicProfileName() {
@ -266,6 +273,10 @@
return false;
}
function FTUECreateUpdatePlayBackProfile() {
return true;
}
function RegisterVolChangeCallBack(functionName) {
dbg('RegisterVolChangeCallBack');
}
@ -444,6 +455,10 @@
];
var response = [];
for (var i=0; i<mixerIds.length; i++) {
// for testing no inputs, set simulateNoInputs = true
if(simulateNoInputs && i == 2) continue;
response.push({
client_id: clientIds[i],
group_id: groups[i],
@ -1012,6 +1027,7 @@
this.FTUELoadAudioConfiguration = FTUELoadAudioConfiguration;
this.FTUEClearChannelAssignments = FTUEClearChannelAssignments;
this.FTUEClearChatInput = FTUEClearChatInput;
this.FTUECreateUpdatePlayBackProfile = FTUECreateUpdatePlayBackProfile;
// Session
this.SessionAddTrack = SessionAddTrack;

View File

@ -1499,9 +1499,18 @@
dataType: "json",
contentType: 'application/json'
});
}
}
function getBackingTracks(options) {
function getSalesHistory(options) {
return $.ajax({
type: "GET",
url: '/api/sales?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function getBackingTracks(options) {
return $.ajax({
type: "GET",
url: '/api/backing_tracks?' + $.param(options),
@ -1765,6 +1774,7 @@
this.getJamtracks = getJamtracks;
this.getPurchasedJamTracks = getPurchasedJamTracks;
this.getPaymentHistory = getPaymentHistory;
this.getSalesHistory = getSalesHistory;
this.getJamTrackRight = getJamTrackRight;
this.enqueueJamTrack = enqueueJamTrack;
this.getBackingTracks = getBackingTracks;

View File

@ -61,7 +61,6 @@ context.JK.JamTrackScreen=class JamTrackScreen
for v in raw_vars
[key, val] = v.split("=")
params[key] = decodeURIComponent(val)
ms
params
refresh:() =>

View File

@ -34,6 +34,7 @@
var $sliderBar = $('.recording-playback', $parentElement);
var $slider = $('.recording-slider', $parentElement);
var $playmodeButton = $('.playback-mode-buttons.icheckbuttons input', $parentElement);
var $jamTrackGetReady = $('.jam-track-get-ready', $parentElement);
var $self = $(this);
@ -158,7 +159,9 @@
setPlaybackMode(playmode);
});
function styleControls( ) {
function styleControls() {
$jamTrackGetReady.attr('data-mode', playbackMonitorMode);
$parentElement.removeClass('mediafile-mode jamtrack-mode metronome-mode');
if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.MEDIA_FILE) {
$parentElement.addClass('mediafile-mode');
@ -194,6 +197,18 @@
positionMs = 0;
}
if(playbackMonitorMode = PLAYBACK_MONITOR_MODE.JAMTRACK) {
if(isPlaying) {
$jamTrackGetReady.attr('data-current-time', positionMs)
}
else {
// this is so the jamtrack 'Get Ready!' stays hidden when it's not playing
$jamTrackGetReady.attr('data-current-time', -1)
}
}
if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) {
updateIsPlaying(isPlaying);
}

View File

@ -5,7 +5,7 @@
context.JK = context.JK || {};
context.JK.CreateScheduledSession = function(app) {
var gearUtils = context.JK.GearUtils;
var gearUtils = context.JK.GearUtilsInstance;
var sessionUtils = context.JK.SessionUtils;
var logger = context.JK.logger;
var rest = JK.Rest();
@ -597,7 +597,9 @@
if(willOptionStartSession()) {
gearUtils.guardAgainstInvalidConfiguration(app)
var shouldVerifyNetwork = createSessionSettings.musician_access.value != 'only-rsvp';
gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork)
.fail(function() {
$btn.removeClass('disabled')
app.notify(
@ -908,6 +910,13 @@
createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_QUICK_START %>';
}
function optionRequiresMultiplayerProfile() {
return createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_START_SCHEDULED%>' ||
createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_IMMEDIATE %>' ||
createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_RSVP %>' ||
createSessionSettings.createType == '<%= MusicSession::CREATE_TYPE_SCHEDULE_FUTURE %>';
}
function next(event) {
if(willOptionStartSession()) {
if(!context.JK.guardAgainstBrowser(app)) {
@ -915,6 +924,12 @@
}
}
if(optionRequiresMultiplayerProfile()) {
if(context.JK.guardAgainstSinglePlayerProfile(app).canPlay == false) {
return false;
}
}
var valid = beforeMoveStep();
if (!valid) {
return false;

View File

@ -108,6 +108,8 @@
var $screen = null;
var $mixModeDropdown = null;
var $templateMixerModeChange = null;
var $myTracksNoTracks = null;
var $otherAudioContainer = null;
var $myTracksContainer = null;
var $liveTracksContainer = null;
@ -121,6 +123,9 @@
var $liveTracks = null;
var $audioTracks = null;
var $fluidTracks = null;
var $voiceChat = null;
var $openFtue = null;
var $tracksHolder = null;
var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup];
var muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup];
@ -197,53 +202,85 @@
// body-scoped drag handlers can go active
screenActive = true;
gearUtils.guardAgainstInvalidConfiguration(app)
.fail(function() {
promptLeave = false;
window.location = '/client#/home'
})
.done(function(){
var result = sessionUtils.SessionPageEnter();
rest.getSessionHistory(data.id)
.done(function(musicSession) {
gearUtils.guardAgainstActiveProfileMissing(app, result)
.fail(function(data) {
var singlePlayerCheckOK = true;
// to know whether we are allowed to be in this session, we have to check if we are the creator when checking against single player functionality
if(musicSession.user_id != context.JK.currentUserId) {
var canPlay = context.JK.guardAgainstSinglePlayerProfile(app, function () {
promptLeave = false;
if(data && data.reason == 'handled') {
if(data.nav == 'BACK') {
window.history.go(-1);
}
else {
window.location = data.nav;
}
}
else {
window.location = '/client#/home';
}
})
.done(function(){
});
sessionModel.waitForSessionPageEnterDone()
.done(function(userTracks) {
singlePlayerCheckOK = canPlay.canPlay;
}
if(singlePlayerCheckOK) {
context.JK.CurrentSessionModel.setUserTracks(userTracks);
var shouldVerifyNetwork = musicSession.musician_access;
gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork)
.fail(function() {
promptLeave = false;
window.location = '/client#/home'
})
.done(function(){
var result = sessionUtils.SessionPageEnter();
initializeSession();
})
.fail(function(data) {
if(data == "timeout") {
context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.')
}
else if(data == 'session_over') {
// do nothing; session ended before we got the user track info. just bail
}
else {
contetx.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data)
}
gearUtils.guardAgainstActiveProfileMissing(app, result)
.fail(function(data) {
promptLeave = false;
if(data && data.reason == 'handled') {
if(data.nav == 'BACK') {
window.history.go(-1);
}
else {
window.location = data.nav;
}
}
else {
window.location = '/client#/home';
}
})
.done(function(){
sessionModel.waitForSessionPageEnterDone()
.done(function(userTracks) {
context.JK.CurrentSessionModel.setUserTracks(userTracks);
initializeSession();
})
.fail(function(data) {
if(data == "timeout") {
context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.')
}
else if(data == 'session_over') {
// do nothing; session ended before we got the user track info. just bail
}
else {
context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data)
}
promptLeave = false;
window.location = '/client#/home'
});
})
})
}
else {
if(canPlay.dialog) {
canPlay.dialog.one(EVENTS.DIALOG_CLOSED, function(e, data) {
if(data.canceled) {
promptLeave = false;
window.location = '/client#/home'
});
})
}
})
}
}
})
.fail(function() {
})
}
function notifyWithUserInfo(title , text, clientId) {
@ -636,7 +673,6 @@
function renderSession() {
$myTracksContainer.empty();
$('.session-track').remove(); // Remove previous tracks
var $voiceChat = $('#voice-chat');
$voiceChat.hide();
_updateMixers();
_renderTracks();
@ -934,7 +970,6 @@
if(voiceChatMixers) {
var mixer = voiceChatMixers.mixer;
var $voiceChat = $('#voice-chat');
$voiceChat.show();
$voiceChat.attr('mixer-id', mixer.id);
var $voiceChatGain = $voiceChat.find('.voicechat-gain');
@ -1655,79 +1690,87 @@
var myTrack = app.clientId == participant.client_id;
// special case; if it's me and I have no tracks, show info about this sort of use of the app
if (myTrack && participant.tracks.length == 0) {
$tracksHolder.addClass('no-local-tracks')
}
else {
$tracksHolder.removeClass('no-local-tracks')
}
// loop through all tracks for each participant
$.each(participant.tracks, function(index, track) {
var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);
$.each(participant.tracks, function (index, track) {
var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);
// Default trackData to participant + no Mixer state.
var trackData = {
trackId: track.id,
connection_id: track.connection_id,
client_track_id: track.client_track_id,
client_resource_id: track.client_resource_id,
clientId: participant.client_id,
name: name,
instrumentIcon: instrumentIcon,
avatar: photoUrl,
latency: "good",
gainPercent: 0,
muteClass: 'muted',
mixerId: "",
avatarClass: 'avatar-med',
preMasteredClass: "",
myTrack: myTrack
};
// Default trackData to participant + no Mixer state.
var trackData = {
trackId: track.id,
connection_id: track.connection_id,
client_track_id: track.client_track_id,
client_resource_id: track.client_resource_id,
clientId: participant.client_id,
name: name,
instrumentIcon: instrumentIcon,
avatar: photoUrl,
latency: "good",
gainPercent: 0,
muteClass: 'muted',
mixerId: "",
avatarClass: 'avatar-med',
preMasteredClass: "",
myTrack: myTrack
};
var mixerData = findMixerForTrack(participant.client_id, track, myTrack)
var mixer = mixerData.mixer;
var vuMixer = mixerData.vuMixer;
var muteMixer = mixerData.muteMixer;
var oppositeMixer = mixerData.oppositeMixer;
var mixerData = findMixerForTrack(participant.client_id, track, myTrack)
var mixer = mixerData.mixer;
var vuMixer = mixerData.vuMixer;
var muteMixer = mixerData.muteMixer;
var oppositeMixer = mixerData.oppositeMixer;
if (mixer && oppositeMixer) {
myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup);
if(!myTrack) {
// it only makes sense to track 'audio established' for tracks that don't belong to you
sessionModel.setAudioEstablished(participant.client_id, true);
}
var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
var muteClass = "enabled";
if (mixer.mute) {
muteClass = "muted";
}
trackData.gainPercent = gainPercent;
trackData.muteClass = muteClass;
trackData.mixerId = mixer.id;
trackData.vuMixerId = vuMixer.id;
trackData.oppositeMixer = oppositeMixer;
trackData.muteMixerId = muteMixer.id;
trackData.noaudio = false;
trackData.group_id = mixer.group_id;
context.jamClient.SessionSetUserName(participant.client_id,name);
} else { // No mixer to match, yet
lookingForMixers.push({track: track, clientId: participant.client_id})
trackData.noaudio = true;
if (!(lookingForMixersTimer)) {
logger.debug("waiting for mixer to show up for track: " + track.id)
lookingForMixersTimer = context.setInterval(lookForMixers, 500);
}
if (mixer && oppositeMixer) {
myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup);
if (!myTrack) {
// it only makes sense to track 'audio established' for tracks that don't belong to you
sessionModel.setAudioEstablished(participant.client_id, true);
}
var allowDelete = myTrack && index > 0;
_addTrack(allowDelete, trackData, mixer, oppositeMixer);
// Show settings icons only for my tracks
if (myTrack) {
myTracks.push(trackData);
var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left);
var muteClass = "enabled";
if (mixer.mute) {
muteClass = "muted";
}
trackData.gainPercent = gainPercent;
trackData.muteClass = muteClass;
trackData.mixerId = mixer.id;
trackData.vuMixerId = vuMixer.id;
trackData.oppositeMixer = oppositeMixer;
trackData.muteMixerId = muteMixer.id;
trackData.noaudio = false;
trackData.group_id = mixer.group_id;
context.jamClient.SessionSetUserName(participant.client_id, name);
} else { // No mixer to match, yet
lookingForMixers.push({track: track, clientId: participant.client_id})
trackData.noaudio = true;
if (!(lookingForMixersTimer)) {
logger.debug("waiting for mixer to show up for track: " + track.id)
lookingForMixersTimer = context.setInterval(lookForMixers, 500);
}
}
var allowDelete = myTrack && index > 0;
_addTrack(allowDelete, trackData, mixer, oppositeMixer);
// Show settings icons only for my tracks
if (myTrack) {
myTracks.push(trackData);
}
});
});
configureTrackDialog = new context.JK.ConfigureTrackDialog(app, myTracks, sessionId, sessionModel);
@ -1880,14 +1923,14 @@
if (!(mixer.stereo)) { // mono track
if (mixerId.substr(-4) === "_vul") {
// Do the left
selector = $('#tracks [mixer-id="' + pureMixerId + '_vul"]');
selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vul"]');
context.JK.VuHelpers.updateVU(selector, value);
// Do the right
selector = $('#tracks [mixer-id="' + pureMixerId + '_vur"]');
selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vur"]');
context.JK.VuHelpers.updateVU(selector, value);
} // otherwise, it's a mono track, _vur event - ignore.
} else { // stereo track
selector = $('#tracks [mixer-id="' + mixerId + '"]');
selector = $tracksHolder.find('[mixer-id="' + mixerId + '"]');
context.JK.VuHelpers.updateVU(selector, value);
}
}
@ -2608,6 +2651,8 @@
var jamTrack = data.result.jamTrack;
$('.session-recording-name').text('');
// hide 'other audio' placeholder
otherAudioFilled();
@ -3048,11 +3093,16 @@
return true;
}
function showFTUEWhenNoInputs( ) {
//app.afterFtue = function() { window.location.reload };
app.layout.startNewFtue();
}
function events() {
$('#session-leave').on('click', sessionLeave);
$('#session-resync').on('click', sessionResync);
$('#session-contents').on("click", '[action="delete"]', deleteSession);
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
$tracksHolder.on('click', 'div[control="mute"]', toggleMute);
$('#recording-start-stop').on('click', startStopRecording);
$('#open-a-recording').on('click', openRecording);
$('#open-a-jamtrack').on('click', openJamTrack);
@ -3061,11 +3111,24 @@
$('#session-invite-musicians').on('click', inviteMusicians);
$('#session-invite-musicians2').on('click', inviteMusicians);
$('#track-settings').click(function() {
if(gearUtils.isNoInputProfile()) {
// show FTUE
showFTUEWhenNoInputs();
return false;
}
else {
configureTrackDialog.refresh();
configureTrackDialog.showVoiceChatPanel(true);
configureTrackDialog.showMusicAudioPanel(true);
}
});
$openFtue.click(function() {
showFTUEWhenNoInputs();
return false;
})
$closePlaybackRecording.on('click', closeOpenMedia);
$(playbackControls)
.on('pause', onPause)
@ -3106,6 +3169,8 @@
$mixModeDropdown = $screen.find('select.monitor-mode')
$templateMixerModeChange = $('#template-mixer-mode-change');
$otherAudioContainer = $('#session-recordedtracks-container');
$myTracksNoTracks = $('#session-mytracks-notracks')
$openFtue = $screen.find('.open-ftue-no-tracks')
$myTracksContainer = $('#session-mytracks-container')
$liveTracksContainer = $('#session-livetracks-container');
$closePlaybackRecording = $('#close-playback-recording')
@ -3116,7 +3181,9 @@
$myTracks = $screen.find('.session-mytracks');
$liveTracks = $screen.find('.session-livetracks');
$audioTracks = $screen.find('.session-recordings');
$fluidTracks = $screen.find('.session-fluidtracks')
$fluidTracks = $screen.find('.session-fluidtracks');
$voiceChat = $screen.find('#voice-chat');
$tracksHolder = $screen.find('#tracks')
events();

View File

@ -12,6 +12,7 @@
var ALERT_TYPES = context.JK.ALERT_TYPES;
var EVENTS = context.JK.EVENTS;
var MIX_MODES = context.JK.MIX_MODES;
var gearUtils = context.JK.GearUtilsInstance;
var userTracks = null; // comes from the backend
var clientId = client.clientID;
@ -213,7 +214,8 @@
// see if we already have tracks; if so, we need to run with these
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
if(inputTracks.length > 0) {
if(inputTracks.length > 0 || gearUtils.isNoInputProfile() ) {
logger.debug("on page enter, tracks are already available")
sessionPageEnterDeferred.resolve(inputTracks);
var deferred = sessionPageEnterDeferred;

View File

@ -136,18 +136,20 @@
return false;
}
gearUtils.guardAgainstInvalidConfiguration(app)
.fail(function() {
app.notify(
{ title: "Unable to Join Session",
text: "You can only join a session once you have working audio gear and a tested internet connection."
});
})
.done(function() {
if (successCallback) {
successCallback();
}
});
if(context.JK.guardAgainstSinglePlayerProfile(app).canPlay) {
gearUtils.guardAgainstInvalidConfiguration(app)
.fail(function() {
app.notify(
{ title: "Unable to Join Session",
text: "You can only join a session once you have working audio gear and a tested internet connection."
});
})
.done(function() {
if (successCallback) {
successCallback();
}
});
}
}
sessionUtils.joinSession = function(sessionId) {

View File

@ -621,11 +621,12 @@
}
// returns Fri May 20, 2013
context.JK.formatDate = function (dateString) {
context.JK.formatDate = function (dateString, suppressDay) {
var date = new Date(dateString);
return days[date.getDay()] + ' ' + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear();
return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear();
}
context.JK.formatDateYYYYMMDD = function(dateString) {
var date = new Date(dateString);
return date.getFullYear() + '-' + context.JK.padString((date.getMonth() + 1).toString(), 2) + '-' + context.JK.padString(date.getDate(), 2);
@ -1111,7 +1112,7 @@
context.JK.guardAgainstBrowser = function(app, args) {
if(!gon.isNativeClient) {
logger.debug("guarding against normal browser on screen thaht requires native client")
logger.debug("guarding against normal browser on screen that requires native client")
app.layout.showDialog('launch-app-dialog', args)
.one(EVENTS.DIALOG_CLOSED, function() {
if(args && args.goHome) {
@ -1124,6 +1125,111 @@
return true;
}
context.JK.guardAgainstSinglePlayerProfile = function(app, beforeCallback) {
var canPlayWithOthers = context.JK.GearUtilsInstance.canPlayWithOthers();
if(!canPlayWithOthers.canPlay) {
logger.debug("guarding against single player profile")
var $dialog = app.layout.showDialog('single-player-profile-dialog');
// so that callers can check dialog result
canPlayWithOthers.dialog = $dialog;
// allow callers to take action before default behavior
if(beforeCallback) {
$dialog.one(EVENTS.DIALOG_CLOSED, beforeCallback);
}
$dialog.one(EVENTS.DIALOG_CLOSED, function(e, data) {
if(!data.canceled) {
if(data.result.choice == 'private_session') {
var data = {
createType: 'quick-start',
timezone: {},
recurring_mode: {},
language: {},
band: {},
musician_access: {},
fans_access: {},
rsvp_slots: [],
open_rsvps: false
};
context.JK.privateSessionSettings(data)
context.JK.createSession(app, data)
.done(function(response) {
var sessionId = response.id;
context.JK.GA.trackSessionCount(true, true, 0);
// we redirect to the session screen, which handles the REST call to POST /participants.
logger.debug("joining session screen: " + sessionId)
context.location = '/client#/session/' + sessionId;
})
.fail(function(jqXHR) {
logger.debug("unable to schedule a private session")
app.notifyServerError(jqXHR, "Unable to schedule a private session");
})
}
else if(data.result.choice == 'gear_setup') {
window.location = '/client#/account/audio'
}
else
{
logger.error("unknown choice: " + data.result.choice)
alert("unknown choice: " + data.result.choice)
}
}
})
}
return canPlayWithOthers;
}
context.JK.createSession = function(app, data) {
// auto pick an 'other' instrument
var otherId = context.JK.server_to_client_instrument_map.Other.server_id; // get server ID
var otherInstrumentInfo = context.JK.instrument_id_to_instrument[otherId]; // get display name
var beginnerLevel = 1; // default to beginner
var instruments = [ {id: otherId, name: otherInstrumentInfo.display, level: beginnerLevel} ];
$.each(instruments, function(index, instrument) {
var slot = {};
slot.instrument_id = instrument.id;
slot.proficiency_level = instrument.level;
slot.approve = true;
data.rsvp_slots.push(slot);
});
data.isUnstructuredRsvp = true;
return rest.createScheduledSession(data)
}
context.JK.privateSessionSettings = function(createSessionSettings) {
createSessionSettings.genresValues = ['Pop'];
createSessionSettings.genres = ['pop'];
createSessionSettings.timezone = 'Central Time (US & Canada),America/Chicago'
createSessionSettings.name = "Private Test Session";
createSessionSettings.description = "Private session set up just to test things out in the session interface by myself.";
createSessionSettings.notations = [];
createSessionSettings.language = 'eng'
createSessionSettings.legal_policy = 'Standard';
createSessionSettings.musician_access = false
createSessionSettings.fan_access = false
createSessionSettings.fan_chat = false
createSessionSettings.approval_required = false
createSessionSettings.legal_terms = true
createSessionSettings.recurring_mode = 'once';
createSessionSettings.start = new Date().toDateString() + ' ' + context.JK.formatUtcTime(new Date(), false);
createSessionSettings.duration = "60";
createSessionSettings.open_rsvps = false
createSessionSettings.rsvp_slots = [];
}
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.

View File

@ -0,0 +1,18 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
var rest = context.JK.Rest();
var logger = context.JK.logger;
function initialize() {
if(gon.signed_in) {
window.location = "/client#/home"
}
}
context.JK.HomePage = initialize;
})(window, jQuery);

View File

@ -61,7 +61,7 @@
//= require web/sessions
//= require web/session_info
//= require web/recordings
//= require web/welcome
//= require web/home
//= require web/individual_jamtrack
//= require web/individual_jamtrack_band
//= require fakeJamClient

View File

@ -176,6 +176,10 @@
wizard.setBackState(enabled);
}
function moveToNext() {
wizard.moveToNext();
}
function setChosenInputs(_inputs) {
inputs = _inputs;
}
@ -222,6 +226,7 @@
this.getChosenInputs = getChosenInputs;
this.setNextState = setNextState;
this.setBackState = setBackState;
this.moveToNext = moveToNext;
this.initialize = initialize;
this.createFTUEProfile = createFTUEProfile;
this.getWizard = function() {return wizard; }

View File

@ -9,7 +9,8 @@
var logger = context.JK.logger;
var networkTest = new context.JK.NetworkTest(app);
var $step = null;
// if not null and with in say 5 seconds, then the user is 'NEXT'ing too quickly. slow them down
var clickFastTime = null;
function getLastNetworkFailAnalytics() {
return networkTest.getLastNetworkFailure();
@ -36,13 +37,29 @@
initializeBackButtonState();
}
function initializeNextButtonState() {
dialog.setNextState(networkTest.hasScoredNetworkSuccessfully() && !networkTest.isScoring());
dialog.setNextState(!networkTest.isScoring());
}
function initializeBackButtonState() {
dialog.setBackState(!networkTest.isScoring());
}
function handleNext() {
// if we don't have a valid score, and if it's been less than 5 seconds since we've shown this step, slow the user down
if (context.jamClient.GetNetworkTestScore() < 1 && userIsFastNexting()) {
context.JK.Banner.showYesNo({
html: "By clicking NEXT and skipping the test, you won't be able to play online in real-time sessions with others. Is this OK?",
yes: function() {
dialog.moveToNext();
}});
return false;
}
else {
return true;
}
}
function handleHelp() {
return "https://jamkazam.desk.com/customer/portal/articles/1716139-what-to-do-if-you-cannot-pass-the-network-test"
//return "https://jamkazam.desk.com/customer/portal/articles/1599969-first-time-setup---step-6---test-your-network";
@ -57,6 +74,16 @@
networkTest.haltScoring();
networkTest.cancel();
updateButtons();
watchForFastNexting();
}
// fast nexting is a someone hitting next very quickly
function watchForFastNexting() {
clickFastTime = new Date();
}
function userIsFastNexting() {
return new Date().getTime() - clickFastTime.getTime() < 5000
}
function beforeHide() {
@ -77,6 +104,7 @@
this.handleHelp = handleHelp;
this.newSession = newSession;
this.beforeHide = beforeHide;
this.handleNext = handleNext;
this.beforeShow = beforeShow;
this.initialize = initialize;
this.getLastNetworkFailAnalytics = getLastNetworkFailAnalytics;

View File

@ -46,6 +46,8 @@
var $templateDeviceNotValid = null;
var $resyncStatus = null;
var $resyncStatusText = null;
var $latencyScoreBox = null;
var $highLatencyNotice = null;
var operatingSystem = null;
@ -579,6 +581,11 @@
function initializeResync() {
$resyncBtn.unbind('click').click(function () {
if($highLatencyNotice) {
$highLatencyNotice.btOff()
$highLatencyNotice = null;
}
scheduleRescanSystem(function() {
if (getSelectedInputs().length > 0 && getSelectedOutputs().length == 2) {
logger.debug("after rescan, ready to attempt score")
@ -946,6 +953,21 @@
queueUpdateDeviceList = false;
updateDeviceList();
}
if(!data.validLatencyScore) {
if (selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1) {
prodUserAboutHighLatency($latencyScoreBox, 'asio')
}
else if (selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) {
prodUserAboutHighLatency($latencyScoreBox, 'asio')
}
else if (selectedDeviceInfo.input.info.type == 'MacOSX_builtin' || selectedDeviceInfo.output.info.type == 'MacOSX_builtin') {
prodUserAboutHighLatency($latencyScoreBox, 'macosx-builtin')
}
else {
prodUserAboutHighLatency($latencyScoreBox, 'generic')
}
}
}
function getLastAudioTestFailAnalytics() {
@ -962,6 +984,13 @@
}
}
function prodUserAboutHighLatency($btn, additional) {
setTimeout(function() {
$highLatencyNotice = context.JK.prodBubble($btn, 'high-latency-notice', {additional: additional}, {duration: 20000, width:'400px', positions:['top']});
}, 300)
}
function prodUserToTweakASIOSettings($btn) {
setTimeout(function() {
context.JK.prodBubble($btn, 'tweak-asio-settings', {}, {positions:['top']});
@ -972,19 +1001,11 @@
renderScoringStopped();
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
if(data.reason == "latency") {
if(data.reason == "io") {
console.log("selectedDeviceInfo", selectedDeviceInfo)
if(selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1) {
prodUserToTweakASIOSettings($asioInputControlBtn)
}
else if(selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) {
prodUserToTweakASIOSettings($asioOutputControlBtn)
}
storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore);
}
else if(data.reason = "io") {
//storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore);
if(data.ioTarget == 'bad') {
storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioTarget, data.ioTargetScore);
}
@ -1210,6 +1231,7 @@
$instructions = $step.find('.instructions');
$resyncStatus = $step.find('.resync-status');
$resyncStatusText = $step.find('.resynctext');
$latencyScoreBox = $step.find('.latency-score-section')
operatingSystem = context.JK.GetOSAsString();
frameBuffers.initialize($knobs);
$(frameBuffers)

View File

@ -52,7 +52,7 @@
var GEAR_TEST_INVALIDATED_ASYNC = "gear_test.async_invalidated"; // happens when backend alerts us device is invalid
function isGoodFtue() {
return validLatencyScore && validIOScore && !asynchronousInvalidDevice;
return validIOScore && !asynchronousInvalidDevice;
}
function processIOScore(io) {
@ -90,7 +90,7 @@
// now base the overall IO score based on both values.
$self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass})
$self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass, validLatencyScore: validLatencyScore})
//renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass);
if(aggregrateIOClass == "bad") {
@ -103,10 +103,10 @@
scoring = false;
if(isGoodFtue()) {
$self.triggerHandler(GEAR_TEST_DONE)
$self.triggerHandler(GEAR_TEST_DONE, {validLatencyScore: validLatencyScore})
}
else {
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std});
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std, validLatencyScore: validLatencyScore});
}
}
@ -182,11 +182,10 @@
updateScoreReport(latency, refocused);
// if there was a valid latency score, go on to the next step
if (validLatencyScore) {
if (true || validLatencyScore) {
$self.triggerHandler(GEAR_TEST_IO_START);
// reuse valid IO score if this is on refocus
if(refocused && validIOScore) {
if(false && (refocused && validIOScore)) {
processIOScore(ioScore);
}
else {
@ -215,12 +214,12 @@
}
else {
scoring = false;
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', latencyScore: latencyScore.latency})
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', validLatencyScore: validLatencyScore, latencyScore: latencyScore.latency})
}
})
.fail(function(ftueSaveResult) {
scoring = false;
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', data: ftueSaveResult})
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', validLatencyScore: validLatencyScore, data: ftueSaveResult})
})
}, 250);
}

View File

@ -14,6 +14,8 @@
var VOICE_CHAT = context.JK.VOICE_CHAT;
var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
var EVENTS = context.JK.EVENTS;
var SYSTEM_DEFAULT_PLAYBACK_ONLY = 'System Default (Playback Only)';
context.JK.GearUtilsInstance = gearUtils;
@ -28,12 +30,42 @@
return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0;
}
// to play with others, you have to have inputs,
// as well have a score below 20 ms
gearUtils.canPlayWithOthers = function(profile) {
gearUtils.createProfileName = function(deviceInfo, chatName) {
var isNoInputProfile = gearUtils.isNoInputProfile(profile);
var expectedLatency = context.jamClient.FTUEGetExpectedLatency();
var audioLatency = expectedLatency ? expectedLatency.latency : null;
var highLatency = audioLatency > 20;
var networkScore = context.jamClient.GetNetworkTestScore();
var badNetworkScore = networkScore < 2;
return {
canPlay: !isNoInputProfile && !highLatency,
isNoInputProfile: isNoInputProfile,
badNetworkScore: badNetworkScore,
highLatency: highLatency,
audioLatency: audioLatency,
networkScore: networkScore,
}
}
gearUtils.isNoInputProfile = function(profile) {
if (profile === undefined) {
profile = context.jamClient.FTUEGetMusicProfileName();
}
if(profile == SYSTEM_DEFAULT_PLAYBACK_ONLY) {
return true;
}
}
gearUtils.createProfileName = function (deviceInfo, chatName) {
var isSameInOut = deviceInfo.input.id == deviceInfo.output.id;
var name = null;
if(isSameInOut) {
if (isSameInOut) {
name = "In/Out: " + deviceInfo.input.info.displayName;
}
else {
@ -45,19 +77,19 @@
}
gearUtils.selectedDeviceInfo = function(audioInputDeviceId, audioOutputDeviceId, deviceInformation) {
gearUtils.selectedDeviceInfo = function (audioInputDeviceId, audioOutputDeviceId, deviceInformation) {
if(!audioInputDeviceId) {
if (!audioInputDeviceId) {
logger.debug("gearUtils.selectedDeviceInfo: no active input device");
return null;
}
if(!audioOutputDeviceId) {
if (!audioOutputDeviceId) {
logger.debug("gearUtils.selectedDeviceInfo: no active output device");
return null;
}
if(!deviceInformation) {
if (!deviceInformation) {
deviceInformation = gearUtils.loadDeviceInfo();
}
@ -81,7 +113,7 @@
}
}
gearUtils.loadDeviceInfo = function() {
gearUtils.loadDeviceInfo = function () {
var operatingSystem = context.JK.GetOSAsString();
// should return one of:
@ -128,6 +160,10 @@
return;
}
if (device.name == "JamKazam Virtual Input") {
return;
}
var deviceInfo = {};
deviceInfo.id = device.guid;
@ -145,22 +181,22 @@
return loadedDevices;
}
gearUtils.updateDefaultBuffers = function(selectedDeviceInfo, frameBuffers) {
gearUtils.updateDefaultBuffers = function (selectedDeviceInfo, frameBuffers) {
function hasWDMAssociated() {
return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')
return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')
}
function hasASIOAssociated() {
return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_asio' || selectedDeviceInfo.output.info.type == 'Win32_asio')
return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_asio' || selectedDeviceInfo.output.info.type == 'Win32_asio')
}
// handle specific framesize settings
if(hasWDMAssociated() || hasASIOAssociated()) {
if (hasWDMAssociated() || hasASIOAssociated()) {
var framesize = frameBuffers.selectedFramesize();
if(framesize == 2.5) {
if (framesize == 2.5) {
// if there is a WDM device, start off at 1/1 due to empirically observed issues with 0/0
if(hasWDMAssociated()) {
if (hasWDMAssociated()) {
logger.debug("setting default buffers to 1/1");
frameBuffers.setBufferIn('1');
frameBuffers.setBufferOut('1');
@ -172,15 +208,15 @@
frameBuffers.setBufferOut('0');
}
}
else if(framesize == 5) {
else if (framesize == 5) {
logger.debug("setting default buffers to 3/2");
frameBuffers.setBufferIn('3');
frameBuffers.setBufferOut('2');
}
else {
logger.debug("setting default buffers to 6/5");
frameBuffers.setBufferIn('6');
frameBuffers.setBufferOut('5');
logger.debug("setting default buffers to 2/2");
frameBuffers.setBufferIn('2');
frameBuffers.setBufferOut('2');
}
}
else {
@ -193,7 +229,7 @@
context.jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
}
gearUtils.ftueSummary = function(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
gearUtils.ftueSummary = function (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
return {
os: operatingSystem,
version: context.jamClient.ClientUpdateVersion(),
@ -203,7 +239,7 @@
validLatencyScore: gearTest.isValidLatencyScore(),
validIOScore: gearTest.isValidIOScore(),
latencyScore: gearTest.getLatencyScore(),
ioScore : gearTest.getIOScore(),
ioScore: gearTest.getIOScore(),
},
audioParameters: {
frameSize: frameBuffers.selectedFramesize(),
@ -221,21 +257,21 @@
* This is to provide a unified view of FTUEGetAllAudioConfigurations & FTUEGetGoodAudioConfigurations
* @returns an array of profiles, where each profile is: {id: profile-name, good: boolean, class: 'bad' | 'good', current: boolean }
*/
gearUtils.getProfiles = function() {
gearUtils.getProfiles = function () {
var all = context.jamClient.FTUEGetAllAudioConfigurations();
var good = context.jamClient.FTUEGetGoodAudioConfigurations();
var current = context.jamClient.LastUsedProfileName();
var profiles = [];
context._.each(all, function(item) {
context._.each(all, function (item) {
profiles.push({id: item, good: false, class:'bad', current: current == item})
profiles.push({id: item, good: false, class: 'bad', current: current == item})
});
if(good) {
for(var i = 0; i < good.length; i++) {
for(var j = 0; j < profiles.length; j++) {
if(good[i] == profiles[j].id) {
if (good) {
for (var i = 0; i < good.length; i++) {
for (var j = 0; j < profiles.length; j++) {
if (good[i] == profiles[j].id) {
profiles[j].good = true;
profiles[j].class = 'good';
break;
@ -246,21 +282,21 @@
return profiles;
}
gearUtils.postDiagnostic = function(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
gearUtils.postDiagnostic = function (operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
rest.createDiagnostic({
type: 'GEAR_SELECTION',
data: {
client_type: context.JK.clientType(),
client_id:
context.JK.JamServer.clientID,
summary:gearUtils.ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated)}
client_id: context.JK.JamServer.clientID,
summary: gearUtils.ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated)
}
});
}
// complete list of possibly chatInputs, whether currently assigned as the chat channel or not
// each item should be {id: channelId, name: channelName, assignment: channel assignment}
gearUtils.getChatInputs = function(){
gearUtils.getChatInputs = function () {
var musicPorts = jamClient.FTUEGetChannels();
//var chatsOnCurrentDevice = context.jamClient.FTUEGetChatInputs(true);
@ -273,11 +309,11 @@
var deDupper = {};
context._.each(musicPorts.inputs, function(input) {
context._.each(musicPorts.inputs, function (input) {
var chatInput = {id: input.id, name: input.name, assignment:input.assignment};
if(!deDupper[input.id]) {
if(input.assignment <= 0) {
var chatInput = {id: input.id, name: input.name, assignment: input.assignment};
if (!deDupper[input.id]) {
if (input.assignment <= 0) {
chatInputs.push(chatInput);
deDupper[input.id] = chatInput;
}
@ -295,9 +331,9 @@
}
})*/
context._.each(chatsOnOtherDevices, function(chatChannelName, chatChannelId) {
context._.each(chatsOnOtherDevices, function (chatChannelName, chatChannelId) {
var chatInput = {id: chatChannelId, name: chatChannelName, assignment: null};
if(!deDupper[chatInput.id]) {
if (!deDupper[chatInput.id]) {
var assignment = context.jamClient.TrackGetAssignment(chatChannelId, true);
chatInput.assignment = assignment;
@ -309,11 +345,11 @@
return chatInputs;
}
gearUtils.isChannelAvailableForChat = function(chatChannelId, musicPorts) {
gearUtils.isChannelAvailableForChat = function (chatChannelId, musicPorts) {
var result = true;
context._.each(musicPorts.inputs, function(inputChannel) {
context._.each(musicPorts.inputs, function (inputChannel) {
// if the channel is currently assigned to a track, it not unassigned
if(inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) {
if (inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) {
result = false;
return false; // break
}
@ -324,13 +360,13 @@
// if the user has a good user network score, immediately returns with a resolved deferred object.
// if not, the user will have the network test dialog prompted... once it's closed, then you'll be told reject() if score is still bad, or resolve() if now good
gearUtils.guardAgainstBadNetworkScore = function(app) {
gearUtils.guardAgainstBadNetworkScore = function (app) {
var deferred = new $.Deferred();
if (!gearUtils.validNetworkScore()) {
// invalid network test score. They have to score to move on
app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, function() {
if(gearUtils.validNetworkScore()) {
app.layout.showDialog('network-test').one(EVENTS.DIALOG_CLOSED, function () {
if (gearUtils.validNetworkScore()) {
deferred.resolve();
}
else {
@ -346,19 +382,19 @@
// XXX this isn't quite right... it needs to check if a good device is *active*
// but seen too many problems so far with the backend not reporting any profile active
gearUtils.hasGoodActiveProfile = function(verifyTracks) {
gearUtils.hasGoodActiveProfile = function (verifyTracks) {
var hasOneConfigureDevice = context.JK.hasOneConfiguredDevice();
logger.debug("hasGoodActiveProfile: " + hasOneConfigureDevice ? "devices='has at least one configured device' " : "devices='has no configured device' ")
return hasOneConfigureDevice;
}
// if the user does not have any profiles, show the FTUE
gearUtils.guardAgainstInvalidGearConfiguration = function(app) {
gearUtils.guardAgainstInvalidGearConfiguration = function (app) {
var deferred = new $.Deferred();
if (context.jamClient.FTUEGetAllAudioConfigurations().length == 0) {
app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, function() {
if(gearUtils.hasGoodActiveProfile() && gearUtils.validNetworkScore()) {
app.layout.showDialog('gear-wizard').one(EVENTS.DIALOG_CLOSED, function () {
if (gearUtils.hasGoodActiveProfile() && gearUtils.validNetworkScore()) {
deferred.resolve();
}
else {
@ -373,27 +409,27 @@
return deferred;
}
gearUtils.guardAgainstActiveProfileMissing = function(app, backendInfo) {
gearUtils.guardAgainstActiveProfileMissing = function (app, backendInfo) {
var deferred = new $.Deferred();
logger.debug("guardAgainstActiveProfileMissing: backendInfo %o", backendInfo);
if(backendInfo.error && backendInfo['reason'] == 'no_profile' && context.jamClient.FTUEGetAllAudioConfigurations().length > 0) {
if (backendInfo.error && backendInfo['reason'] == 'no_profile' && context.jamClient.FTUEGetAllAudioConfigurations().length > 0) {
// if the backend says we have no_profile, but we have profiles , send them to the audio profile screen
// this should be a very rare path
deferred.reject({reason:'handled', nav: '/client#/account/audio'});
deferred.reject({reason: 'handled', nav: '/client#/account/audio'});
context.JK.Banner.showAlert('No Active Profile', 'We\'ve sent you to the audio profile screen to remedy the fact that you have no active audio profile. Please select ACTIVATE on an existing profile, or select ADD NEW GEAR to add a new profile.');
}
else if (backendInfo.error && backendInfo['reason'] == 'device_failure') {
app.layout.showDialog('audio-profile-invalid-dialog')
.one(EVENTS.DIALOG_CLOSED, function(e, data) {
if(!data.result || data.result == 'cancel') {
deferred.reject({reason:'handled', nav: 'BACK'});
.one(EVENTS.DIALOG_CLOSED, function (e, data) {
if (!data.result || data.result == 'cancel') {
deferred.reject({reason: 'handled', nav: 'BACK'});
}
else if(data.result == 'configure_gear'){
deferred.reject({reason:'handled', nav: '/client#/account/audio'});
else if (data.result == 'configure_gear') {
deferred.reject({reason: 'handled', nav: '/client#/account/audio'});
}
else if(data.result == 'session') {
else if (data.result == 'session') {
deferred.resolve();
}
else {
@ -409,43 +445,49 @@
}
// tests both device config, and network score
gearUtils.guardAgainstInvalidConfiguration = function(app) {
gearUtils.guardAgainstInvalidConfiguration = function (app, verifyNetworkScore) {
var deferred = new $.Deferred();
gearUtils.guardAgainstInvalidGearConfiguration(app)
.fail(function() {
.fail(function () {
deferred.reject();
})
.done(function() {
gearUtils.guardAgainstBadNetworkScore(app)
.fail(function() {
deferred.reject();
})
.done(function() {
deferred.resolve();
})
.done(function () {
if(verifyNetworkScore) {
gearUtils.guardAgainstBadNetworkScore(app)
.fail(function () {
deferred.reject();
})
.done(function () {
deferred.resolve();
})
}
else {
deferred.resolve();
}
})
return deferred;
}
gearUtils.skipNetworkTest = function() {
gearUtils.skipNetworkTest = function () {
context.jamClient.SetNetworkTestScore(gearUtils.SKIPPED_NETWORK_TEST);
gearUtils.skippedNetworkTest = true;
}
gearUtils.isNetworkTestSkipped = function() {
gearUtils.isNetworkTestSkipped = function () {
return gearUtils.skippedNetworkTest;
}
gearUtils.validNetworkScore = function() {
gearUtils.validNetworkScore = function () {
return gearUtils.skippedNetworkTest || context.jamClient.GetNetworkTestScore() >= 2;
}
gearUtils.isRestartingAudio = function() {
gearUtils.isRestartingAudio = function () {
return !!reloadAudioTimeout;
}
gearUtils.scheduleAudioRestart = function(location, initial_delay, beforeScan, afterScan, cancelScan) {
gearUtils.scheduleAudioRestart = function (location, initial_delay, beforeScan, afterScan, cancelScan) {
logger.debug("scheduleAudioRestart: (from " + location + ")")
@ -453,40 +495,42 @@
function clearAudioReloadTimer() {
if(!cancellable) {return;}
if (!cancellable) {
return;
}
if(cancelScan) {
if (cancelScan) {
cancelScan();
}
else if(afterScan) {
else if (afterScan) {
afterScan(true);
}
clearTimeout(reloadAudioTimeout);
reloadAudioTimeout = null;
currentAudioRestartLocation = null;
currentAudioRestartLocation = null;
cancellable = false;
}
// refresh timer if outstanding
if(reloadAudioTimeout) {
if (reloadAudioTimeout) {
logger.debug("scheduleAudioRestart: clearing timeout (from " + location + ")")
clearTimeout(reloadAudioTimeout);
}
currentAudioRestartLocation = location;
if(beforeScan) {
if (beforeScan) {
beforeScan();
}
reloadAudioTimeout = setTimeout(function() {
reloadAudioTimeout = setTimeout(function () {
logger.debug("scheduleAudioRestart: rescan beginning (from " + location + ")")
reloadAudioTimeout = null;
currentAudioRestartLocation = null;
cancellable = false;
if(afterScan) {
if (afterScan) {
afterScan(false);
}
}, initial_delay ? initial_delay : 5000);
@ -494,4 +538,45 @@
return clearAudioReloadTimer;
}
gearUtils.bootstrapDefaultPlaybackProfile = function () {
var profiles = gearUtils.getProfiles();
var foundSystemDefaultPlaybackOnly = false
context._.each(profiles, function (profile) {
if (profile.id == SYSTEM_DEFAULT_PLAYBACK_ONLY) {
foundSystemDefaultPlaybackOnly = true
return false;
}
})
if (!foundSystemDefaultPlaybackOnly) {
logger.debug("creating system default profile (playback only")
if(!gearUtils.createDefaultPlaybackOnlyProfile()) {
logger.error("unable to create the default playback profile!");
}
}
}
gearUtils.createDefaultPlaybackOnlyProfile = function () {
var eMixerInputSampleRate = {
JAMKAZAM_AUTO_SR: 0,
USE_DEVICE_DEFAULT_SR: 1,
PREFER_44: 2,
PREFER_48: 3,
PREFER_96: 4
}
// null//upgrade protect
if(context.jamClient.FTUECreateUpdatePlayBackProfile) {
return context.jamClient.FTUECreateUpdatePlayBackProfile(SYSTEM_DEFAULT_PLAYBACK_ONLY,
eMixerInputSampleRate.JAMKAZAM_AUTO_SR,
0, // buffering
false); // start audio
}
else {
return false;
}
}
})(window, jQuery);

View File

@ -69,11 +69,15 @@
if(result === false) {return false;}
}
moveToNext();
return false;
}
function moveToNext() {
previousStep = step;
step = step + 1;
moveToStep();
return false;
}
function help() {
@ -238,6 +242,7 @@
this.getNextButton = getNextButton;
this.setNextState = setNextState;
this.setBackState = setBackState;
this.moveToNext = moveToNext;
this.getCurrentStep = getCurrentStep;
this.getCurrentWizardStep = getCurrentWizardStep;
this.onCloseDialog = onCloseDialog;

View File

@ -0,0 +1,51 @@
@import 'common.css.scss';
#account-payment-history {
.content-body-scroller {
padding:20px;
@include border_box_sizing;
}
table td.loading {
text-align:center;
}
.end-of-list {
margin-top:20px;
}
td {
&.amount {
}
&.voided {
text-decoration:line-through;
}
}
.account-left {
float: left;
min-width: 165px;
width: 20%;
}
.account-left h2 {
color: #FFFFFF;
font-size: 23px;
font-weight: 400;
margin-bottom: 20px;
}
.input-aligner {
margin: 10px 14px 20px 0;
text-align:right;
.back {
margin-right:22px;
}
}
}

View File

@ -31,6 +31,7 @@
*= require ./findSession
*= require ./session
*= require ./account
*= require ./accountPaymentHistory
*= require ./search
*= require ./ftue
*= require ./jamServer

View File

@ -330,3 +330,8 @@ $fair: #cc9900;
border-radius:8px;
}
.capitalize {
text-transform: capitalize
}

View File

@ -10,6 +10,7 @@
.retry {
margin-top:10px;
white-space: normal;
}
.msg {

View File

@ -45,6 +45,10 @@ body.jam, body.web, .dialog{
}
}
.help-high-latency-notice {
width:400px;
}
.help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks {
font-size:12px;

View File

@ -240,8 +240,4 @@
.jamtrack_buttons {
margin: 8px 4px 12px 4px;
}
.capitalize {
text-transform: capitalize
}

View File

@ -68,8 +68,8 @@
vertical-align:top;
}
.session-recordedtracks-container {
//display: block;
.recording {
top:310px; // // this is to prevent scroll bars from pushing this element up
}
.recording-controls {
@ -84,6 +84,32 @@
.recording-current {
top:3px ! important;
}
.jam-track-get-ready {
display:none;
position:absolute;
top:-29px;
margin-left:-50px;
width:100px;
vertical-align:middle;
height:32px;
line-height:32px;
left:50%;
&[data-mode="JAMTRACK"] {
&[data-current-time="0"] {
display:block;
}
}
.spinner-small {
vertical-align:middle;
display:inline-block;
}
span {
vertical-align:middle;
}
}
}
.playback-mode-buttons {
@ -210,6 +236,18 @@
$otheraudio-minwidth:195px;
$otheraudio-open-minwidth:230px;
#session-mytracks-notracks {
display:none;
p {
font-size:14px;
white-space:normal;
margin:10px 10px 0 0;
line-height:125%;
}
}
.session-mytracks {
padding-left:15px;
float:left;
@ -251,6 +289,12 @@
.recording-controls {
min-width:230px;
}
#recording-start-stop {
@include border-radius(4px);
padding-left:5px;
padding-right:5px;
}
}
.session-recordings {
@ -358,6 +402,25 @@
#tracks {
margin-top:12px;
overflow:auto;
&.no-local-tracks {
#session-mytracks-notracks {
display:block;
}
#session-mytracks-container {
display:none;
}
#recording-start-stop {
display:none;
}
#session-invite-musicians {
display:none;
}
}
}
.track-empty a {

View File

@ -429,8 +429,12 @@
}
.instructions {
height: 228px !important;
}
.network-test-results {
height: 248px !important;
height: 228px !important;
@include border_box_sizing;
&.testing {

View File

@ -135,6 +135,25 @@
}
.get-a-free-jamtrack-section {
&.has-free-jamtrack {
h2.get-a-free-jamtrack {
display:block;
}
.action-button {
margin-top:-7px;
}
}
&.no-free-jamtrack {
h2.browse-jamtracks {
display:block;
}
}
}
.ftue-inner table a {
text-decoration:none;
}

View File

@ -0,0 +1,29 @@
#single-player-profile-dialog {
.dialog-inner {
&.high-latency {
.high-latency {
display:block
}
}
&.has-no-inputs {
.has-no-inputs {
display:block
}
}
}
.audio-latency {
font-weight:bold;
}
.action-buttons {
margin:20px 0;
}
p {
line-height:125%;
}
}

View File

@ -0,0 +1,135 @@
@charset "UTF-8";
@import "client/common.css.scss";
body.web.home {
.landing-tag {
margin-top:20px;
}
.logo-home {
margin:20px 0 15px;
}
.home-column {
margin-top: 20px;
width: 345px;
float: left;
margin-right: 30px;
text-align: center;
margin-bottom:35px;
&.last{
margin-right:0;
}
h3 {
text-align:left;
margin-top:12px;
margin-bottom:6px;
font-size:18px;
font-weight:700;
}
p {
color:white;
text-align:left;
line-height:120%;
font-size:13px;
margin-bottom:20px;
}
.extra-links {
width:234px;
display:inline-block;
}
.learn-more {
font-size: 12px;
margin-top: 5px;
&.shared {
float:left;
margin-left:10px;
}
}
.sign-in-holder {
font-size: 12px;
margin-top: 5px;
&.shared {
float:right;
margin-right:10px;
}
}
}
.latest-promo {
float:left;
}
.endorsement-promo {
float:right;
}
.home-buzz {
h2 {
width:100%;
text-align:center;
margin:20px 0;
}
width: 300px;
position:relative;
margin-right:20px;
.buzz-items {
.buzz-item {
padding: 12px 0;
&:last-child {
padding-bottom:0;
}
}
.buzz-item-text {
padding-left: 78px; // 58px width for image + 20px margin
}
}
}
.latest {
width: 750px;
position:relative;
top:-45px;
.home-session-list {
top:5px; // XXX remove post release
width:100%;
height:400px;
border: solid 1px #ed3618;
background-color:#353535;
float:left;
overflow:hidden;
position:relative;
}
.latest-head {
position: absolute;
padding:20px 20px 12px;
height: 53px;
width:inherit;
}
.latest-body {
width:100%;
top:65px;
bottom:0;
position:absolute;
overflow-y:scroll;
@include border_box_sizing;
.session-list-wrapper {
padding: 0 20px;
}
}
}
}

View File

@ -20,6 +20,7 @@
*= require web/footer
*= require web/recordings
*= require web/welcome
*= require web/home
#= require web/sessions
*= require web/events
*= require web/session_info

View File

@ -2,7 +2,7 @@
@import "client/common.css.scss";
body.web {
body.web.welcome {
.signin-common {
height:auto;

View File

@ -18,27 +18,27 @@ class ApiRecurlyController < ApiController
if current_user
# keep reuse card up-to-date
User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card_next_time])
# keep reuse card up-to-date
User.where(id: current_user.id).update_all(reuse_card: params[:reuse_card_next_time])
else
options = {
remote_ip: request.remote_ip,
first_name: billing_info[:first_name],
last_name: billing_info[:last_name],
email: params[:email],
password: params[:password],
password_confirmation: params[:password],
terms_of_service: terms_of_service,
instruments: [{ :instrument_id => 'other', :proficiency_level => 1, :priority => 1 }],
birth_date: nil,
location: { :country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]},
musician: true,
skip_recaptcha: true,
invited_user: nil,
fb_signup: nil,
signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm",
any_user: any_user,
reuse_card: reuse_card_next_time
remote_ip: request.remote_ip,
first_name: billing_info[:first_name],
last_name: billing_info[:last_name],
email: params[:email],
password: params[:password],
password_confirmation: params[:password],
terms_of_service: terms_of_service,
instruments: [{:instrument_id => 'other', :proficiency_level => 1, :priority => 1}],
birth_date: nil,
location: {:country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]},
musician: true,
skip_recaptcha: true,
invited_user: nil,
fb_signup: nil,
signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm",
any_user: any_user,
reuse_card: reuse_card_next_time
}
user = UserManager.new.signup(options)
@ -61,9 +61,9 @@ class ApiRecurlyController < ApiController
@account = @client.find_or_create_account(current_user, billing_info)
end
render :json=>account_json(@account)
render :json => account_json(@account)
rescue RecurlyClientError => x
render json: { :message => x.inspect, errors: x.errors }, :status => 404
render json: {:message => x.inspect, errors: x.errors}, :status => 404
end
end
@ -71,86 +71,80 @@ class ApiRecurlyController < ApiController
@client.delete_account(current_user)
render json: {}, status: 200
rescue RecurlyClientError => x
render json: { :message => x.inspect, errors: x.errors}, :status => 404
render json: {:message => x.inspect, errors: x.errors}, :status => 404
end
# get Recurly account
def get_account
@account = @client.get_account(current_user)
render :json=>account_json(@account)
render :json => account_json(@account)
rescue RecurlyClientError => e
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# get Recurly payment history
def payment_history
@payments=@client.payment_history(current_user)
render :json=>{payments: @payments}
render :json => {payments: @payments}
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# update Recurly account
def update_account
@account=@client.update_account(current_user, params[:billing_info])
render :json=>account_json(@account)
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
@account=@client.update_account(current_user, params[:billing_info])
render :json => account_json(@account)
rescue RecurlyClientError => x
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# get Billing Information
def billing_info
@account = @client.get_account(current_user)
if @account
render :json=> account_json(@account)
render :json => account_json(@account)
else
render :json=> {}, :status => 404
render :json => {}, :status => 404
end
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
# update Billing Information
def update_billing_info
@account=@client.update_billing_info(current_user, params[:billing_info])
render :json=> account_json(@account)
@account = @client.update_billing_info(current_user, params[:billing_info])
render :json => account_json(@account)
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
def place_order
error=nil
response = {jam_tracks:[]}
response = {jam_tracks: []}
sale = Sale.create(current_user)
sales = Sale.place_order(current_user, current_user.shopping_carts)
if sale.valid?
current_user.shopping_carts.each do |shopping_cart|
jam_track = shopping_cart.cart_product
# if shopping_cart has any marked_for_redeem, then we zero out the price by passing in 'free'
# NOTE: shopping_carts have the idea of quantity, but you should only be able to buy at most one JamTrack. So anything > 0 is considered free for a JamTrack
jam_track_right = @client.place_order(current_user, jam_track, shopping_cart, sale)
# build up the response object with JamTracks that were purchased.
# if this gets more complicated, we should switch to RABL
response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version}
sales.each do |sale|
if sale.is_jam_track_sale?
sale.sale_line_items.each do |line_item|
jam_track = line_item.product
jam_track_right = jam_track.right_for_user(current_user)
response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version}
end
end
else
error = 'can not create sale'
end
if error
render json: { errors: {message:error}}, :status => 404
render json: {errors: {message: error}}, :status => 404
else
render :json=>response, :status=>200
render :json => response, :status => 200
end
rescue RecurlyClientError => x
render json: { message: x.inspect, errors: x.errors}, :status => 404
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
private
private
def create_client
@client = RecurlyClient.new
end
@ -173,5 +167,5 @@ private
billing_info: billing_info
}
end
end # class

View File

@ -0,0 +1,15 @@
class ApiSalesController < ApiController
respond_to :json
def index
data = Sale.index(current_user,
page: params[:page],
per_page: params[:per_page])
@sales = data[:query]
@next = data[:next_page]
render "api_sales/index", :layout => nil
end
end

View File

@ -46,6 +46,7 @@ class ApiUsersController < ApiController
@user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments)
@user.update_genres(params[:genres].nil? ? [] : params[:genres]) if params.has_key?(:genres)
@user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next)
@user.show_whats_next_count = params[:show_whats_next_count] if params.has_key?(:show_whats_next_count)
@user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email)
@user.biography = params[:biography] if params.has_key?(:biography)
@user.mod_merge(params[:mods]) if params[:mods]

View File

@ -16,6 +16,7 @@ class ClientsController < ApplicationController
return
end
gon.recurly_tax_estimate_jam_track_plan = Rails.application.config.recurly_tax_estimate_jam_track_plan
render :layout => 'client'
end

View File

@ -94,7 +94,7 @@ class LandingsController < ApplicationController
jam_track = JamTrack.first
end
gon.jam_track_plan_code = jam_track.plan_code
gon.jam_track_plan_code = jam_track.plan_code if jam_track
render 'product_jamtracks', layout: 'web'
end
end

View File

@ -215,7 +215,24 @@ class UsersController < ApplicationController
#@jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014 # development ID
# temporary--end
@welcome_page = true
#@welcome_page = true
render :layout => "web"
end
# DO NOT USE CURRENT_USER IN THIS ROUTINE. IT'S CACHED FOR THE WHOLE SITE
def home
@no_user_dropdown = true
@promo_buzz = PromoBuzz.active
if Rails.application.config.use_promos_on_homepage
@promo_latest = PromoLatest.active
else
@promo_latest, start = Feed.index(nil, limit: 10)
end
gon.signed_in = !current_user.nil?
render :layout => "web"
end
@ -396,6 +413,19 @@ JS
end
end
def unsubscribe
unless @user = User.read_access_token(params[:user_token])
redirect_to '/'
end if params[:user_token].present?
if request.get?
elsif request.post?
@user.subscribe_email = false
@user.save!
end
end
private
def is_native_client

View File

@ -0,0 +1,7 @@
node :next_page do |page|
@next
end
node :entries do |page|
partial "api_sales/show", object: @sales
end

View File

@ -0,0 +1,11 @@
object @sale
attributes :id, :recurly_invoice_id, :recurly_subtotal_in_cents, :recurly_tax_in_cents, :recurly_total_in_cents, :recurly_currency, :sale_type, :recurly_invoice_number, :state, :created_at
child(:recurly_transactions => :recurly_transactions) {
attributes :transaction_type, :amount_in_cents
}
child(:sale_line_items => :line_items) {
attributes :id, :product_info
}

View File

@ -1,6 +1,6 @@
object @user
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :reuse_card, :purchased_jamtracks_count
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count
if @user.musician?
node :location do @user.location end
@ -10,7 +10,7 @@ end
# give back more info if the user being fetched is yourself
if @user == current_user
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter, :new_notifications
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :show_whats_next_count, :subscribe_email, :auth_twitter, :new_notifications, :sales_count, :reuse_card, :purchased_jamtracks_count
node :geoiplocation do |user|
geoiplocation = current_user.geoiplocation

View File

@ -118,7 +118,13 @@
</div>
<div class="account-mid payments">
<a id="account-payment-history-link" href="#">View Payment History</a>
<div class="whitespace">
{% if (data.sales_count == 0) { %}
You have made no purchases.
{% } else { %}
You have made {{data.sales_count}} purchase{{data.sales_count == 1 ? '' : 's'}}.
{% } %}
</div>
</div>
<div class="right">

View File

@ -0,0 +1,44 @@
.screen.secondary layout="screen" layout-id="account/paymentHistory" class="screen secondary" id="account-payment-history"
.content
.content-head
.content-icon=image_tag("content/icon_account.png", height:20, width:27 )
h1 my account
=render "screen_navigation"
.content-body
.content-body-scroller
.account-left
h2 payment history:
table.payment-table
thead
tr
th DATE
th METHOD
th DESCRIPTION
th STATUS
th AMOUNT
tbody
a.btn-next-pager href="/api/sales?page=1" Next
.end-of-payments-list.end-of-list="No more payment history"
.input-aligner
a.back href="" class="button-grey" BACK
br clear="all"
script#template-payment-history-row type="text/template"
tr
td
| {{data.date}}
td.capitalize
| {{data.payment_method}}
td
| {{data.description}}
td.capitalize
| {{data.status}}
td.amount class="{{data.status}}"
| ${{data.amount}}

View File

@ -115,10 +115,8 @@ script type='text/template' id='template-order-content'
.order-right-page
h2 PLACE ORDER
.recurly-data.hidden
= "{% _.each(data.carts, function(cart) { %}"
.plan data-plan-code="{{cart.product_info.plan_code}}"
input data-recurly="plan" type="text" value="{{cart.product_info.plan_code}}"
= "{% }); %}"
.plan.jamtrack data-plan-code="{{gon.recurly_tax_estimate_jam_track_plan}}"
input data-recurly="plan" type="text" value="{{gon.recurly_tax_estimate_jam_track_plan}}"
.order-summary
.place-order-center
a.button-orange.place-order href="#" PLACE YOUR ORDER

View File

@ -30,6 +30,29 @@ script type="text/template" id="template-help-can-move-on"
script type="text/template" id="template-help-tweak-asio-settings"
| Click here to try faster ASIO settings.
script type="text/template" id="template-help-high-latency-notice"
.help-high-latency-notice
| {% if(data.additional == 'asio') { %}
p.gear-specific-latency-notice Tip: click the ASIO SETTINGS button to try faster ASIO settings.
p
| If you are unable to get your audio gear latency below 20 milliseconds, you can click NEXT to proceed through setup with a high-latency audio profile. This will allow you to play with JamTracks and backing tracks, but not play with others.&nbsp;
p
a href="https://jamkazam.desk.com/customer/portal/articles/1520627-my-audio-gear-won-t-pass-latency-or-i-o-tests" rel="external" Click here
| &nbsp;for more troubleshooting tips to speed up your audio gear setup.
| {% } else if(data.additional == 'macosx-builtin') { %}
p.gear-specific-latency-notice Tip: Insert your headphones on a Mac to bring your latency down, and click the RESYNC button to try again.
p
| If you are unable to get your audio gear latency below 20 milliseconds, you can click NEXT to proceed through setup with a high-latency audio profile. This will allow you to play with JamTracks and backing tracks, but not play with others.&nbsp;
p
a href="https://jamkazam.desk.com/customer/portal/articles/1520627-my-audio-gear-won-t-pass-latency-or-i-o-tests" rel="external" Click here
| &nbsp;for more troubleshooting tips to speed up your audio gear setup.
| {% } else { %}
p.general-info
| Your computer and interface are processing audio too slowly to play online in real-time sessions with other musicians over the Internet. You may click NEXT to proceed through setup to play alone in sessions with JamTracks or backing tracks, or if you want to improve your speed score to play online,&nbsp;
a href="https://jamkazam.desk.com/customer/portal/articles/1520627-my-audio-gear-won-t-pass-latency-or-i-o-tests" rel="external" click here
| &nbsp;for a troubleshooting article.
| {% } %}
script type="text/template" id="template-help-session-plus-musicians"
| Plus any interested JamKazam musicians that I approve.

View File

@ -1,5 +1,5 @@
.network-test
.help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection.
.help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection. If you don't want to play online in real-time sessions, you can click NEXT to skip this step.
.wizard-step-content
.wizard-step-column
%h2 Instructions

View File

@ -1,6 +1,10 @@
<!-- recording play controls -->
<div class="recording recording-controls has-mix">
<div class="jam-track-get-ready">
<div class="spinner-small"></div>
<span>Get Ready!</span>
</div>
<!-- play button -->
<a class="left play-button" href="#">
<%= image_tag "content/icon_playbutton.png", {:height => 20, :width => 20, :class=> "playbutton"} %>

View File

@ -40,6 +40,12 @@
span
| Settings
.session-tracks-scroller
#session-mytracks-notracks
p.notice
| You have not set up any inputs for your instrument or vocals.&nbsp;
| If you want to hear yourself play through the JamKazam app,&nbsp;
| and let the app mix your live playing with JamTracks, or with other musicians in online sessions,&nbsp;
a.open-ftue-no-tracks href='#' click here now.
#session-mytracks-container
#voice-chat.voicechat[style="display:none;" mixer-id=""]
.voicechat-label

View File

@ -58,6 +58,7 @@
<%= render "account_jamtracks" %>
<%= render "account_session_detail" %>
<%= render "account_session_properties" %>
<%= render "account_payment_history" %>
<%= render "inviteMusicians" %>
<%= render "hoverBand" %>
<%= render "hoverFan" %>
@ -214,6 +215,9 @@
var accountAudioProfile = new JK.AccountAudioProfile(JK.app);
accountAudioProfile.initialize();
var accountPaymentHistoryScreen = new JK.AccountPaymentHistoryScreen(JK.app);
accountPaymentHistoryScreen.initialize();
var searchResultScreen = new JK.SearchResultScreen(JK.app);
searchResultScreen.initialize();
@ -249,8 +253,6 @@
var jamtrackScreen = new JK.JamTrackScreen(JK.app);
jamtrackScreen.initialize();
var jamtrackLanding = new JK.JamTrackLanding(JK.app);
jamtrackLanding.initialize();
@ -299,6 +301,9 @@
var allSyncsDialog = new JK.AllSyncsDialog(JK.app);
allSyncsDialog.initialize();
var singlePlayerProfileGuardDialog = new JK.SinglePlayerProfileGuardDialog(JK.app);
singlePlayerProfileGuardDialog.initialize();
// do a client update early check upon initialization
JK.ClientUpdateInstance.check()
@ -312,6 +317,9 @@
var jamServer = new JK.JamServer(JK.app, function(event_type) {JK.app.activeElementEvent(event_type)});
jamServer.initialize();
var clientInit = new JK.ClientInit();
clientInit.init();
// latency_tester does not want to be here - redirect it
if(window.jamClient.getOperatingMode && window.jamClient.getOperatingMode() == "server") {
window.location = "/latency_tester";

View File

@ -37,3 +37,4 @@
= render 'dialogs/openBackingTrackDialog'
= render 'dialogs/loginRequiredDialog'
= render 'dialogs/jamtrackPaymentHistoryDialog'
= render 'dialogs/singlePlayerProfileGuard'

View File

@ -36,16 +36,24 @@
a href="#" class="google-invite"
= image_tag "content/icon_google.png", {:align=>"absmiddle", :height => 26, :width => 26 }
span Google+
.column.get-a-free-jamtrack-section
h2.get-a-free-jamtrack.hidden GET A FREE JAMTRACK
h2.browse-jamtracks.hidden CHECK OUT JAMTRACKS
.blurb
| JamTracks are the best way to play with your favorite music. Unlike traditional backing tracks,&nbsp;
| they are complete multitrack recordings, with fully isolated tracks for each part.&nbsp
span.jamtracks-limited-time.hidden For a limited time, you can get your first JamTrack free.
| Check it out!
.action-button
a.button-orange.browse-jamtrack rel="external" href="#" LEARN MORE
br clear="both"
.row.find-connect
.column
h2 CREATE A "REAL" SESSION
.blurb
| You can create a session to start immediately and hope others join, but this doesnt work well. Its better
to schedule a session and invite friends or the community to join you. Watch a video to learn how, then
schedule your first session!
| You can create sessions that start immediately and see who joins, or you can schedule sessions, invite friends, and others from the community, and manage RSVPs. Learn how.
.action-button
a.button-orange rel="external" href="https://www.youtube.com/watch?v=EZZuGcDUoWk" WATCH VIDEO
br clear="both"
.row.find-connect
.column
h2 FIND SESSIONS TO JOIN
.blurb
@ -53,14 +61,6 @@
to learn about how to find and select good sessions to join.
.action-button
a.button-orange.setup-gear rel="external" href="https://www.youtube.com/watch?v=xWponSJo-GU" WATCH VIDEO
.column
h2 CONNECT WITH MUSICIANS
.blurb
| To play more music, tap into our growing
community to connect with other musicians. Watch this video for tips on how to do this.
.action-button
a.button-orange rel="external" href="https://www.youtube.com/watch?v=4KWklSZZxRc" WATCH VIDEO
br clear="both"
.row.full.learn-more
.column

View File

@ -18,16 +18,3 @@
.jamtrack_buttons
.right
a.button-orange class='btnCancel' layout-action='cancel' OK
script#template-payment-history-row type="text/template"
tr
td
| {{data.date}}
td
| ${{data.amount}}
td.capitalize
| {{data.status}}
td.capitalize
| {{data.payment_method}}
td
| {{data.reference}}

View File

@ -0,0 +1,22 @@
.dialog.dialog-overlay-sm layout='dialog' layout-id='single-player-profile-dialog' id='single-player-profile-dialog'
.content-head
= image_tag "content/icon_alert.png", {:width => 24, :height => 24, :class => 'content-icon' }
h1 Application Notice
.dialog-inner
p.high-latency.hidden
| Your audio profile has a latency score of&nbsp;
span.audio-latency
br
br
| This is too high to play with others in real-time. However, you can play with JamTracks and backing tracks by yourself in a private session, or go to the gear setup wizard and add a new audio profile with lower latency.
p.has-no-inputs.hidden
| You are currently using the default system profile, which has no audio inputs.
br
br
| With this profile, you can't play with others in real-time. However, you can play with JamTracks and backing tracks by yourself in a private session, or go to the gear setup wizard and add a new audio profile that uses your gear.
.right.action-buttons
a.button-grey.btn-cancel href='#' layout-action="cancel" CANCEL
a.button-grey.btn-gear-setup href="/client#/account/audio" GO TO GEAR SETUP
a.button-orange.btn-private-session href="#" PRIVATE SESSION

View File

@ -0,0 +1,63 @@
- provide(:page_name, 'home')
.home-column
= link_to image_tag("web/thumbnail_jamtracks.jpg", :alt => "JamTracks explanatory video"), '#', class: "jamtracks-video video-item", 'data-video-header' => 'JamTracks', 'data-video-url' => 'http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1'
h3 Complete, Multi-Track Backing Tracks
p
strong JamTracks
| &nbsp;are the best way to play with your favorite music. Unlike traditional backing tracks, JamTracks are complete multitrack recordings, with fully isolated tracks for each part.
= link_to image_tag("web/button_cta_jamtrack.png", width: 234, height:57), '/client#/jamtrack', class: 'cta-button jamtracks'
br clear="all"
.extra-links
.learn-more
a.learn-more-jamtracks href='/products/jamtracks' learn more
.home-column
= link_to image_tag("web/thumbnail_platform.jpg", :alt => "JamKazam explanatory video!"), '#', class: "platform-video video-item", 'data-video-header' => 'JamKazam Platform', 'data-video-url' => 'http://www.youtube.com/embed/ylYcvTY9CVo?autoplay=1'
h3 Online Music Collaboration Platform
p
strong JamKazam
| &nbsp;is an innovative live music platform and social network, enabling musicians to play music together in real time from different locations over the Internet as if they are sitting in the same room.
= link_to image_tag("web/button_cta_platform.png", width: 234, height: 57), '/signup', class: 'cta-button platform'
.extra-links
span.learn-more.shared
a.learn-more-platform href='/products/platform' learn more
span.sign-in-holder.shared
a.sign-in href='/signin' sign in
br clear="all"
.home-column.last
= link_to image_tag("web/thumbnail_jamblaster.jpg", :alt => "JamBlaster explanatory video!"), '#', class: "jamblaster-video video-item", 'data-video-header' => 'JamBlaster', 'data-video-url' => 'http://www.youtube.com/embed/gAJAIHMyois?autoplay=1'
h3 Ultra Low-Latency Audio Interface
p
| The&nbsp;
strong JamBlaster
| &nbsp;is a device designed from the ground up to meet the requirements of online music play, vastly extending the range over which musicians can play together across the Internet.
= link_to image_tag("web/button_cta_jamblaster.png", width: 234, height: 57), '/products/jamblaster', class: 'cta-button jamblaster'
.extra-links
.learn-more
a.learn-more-jamblaster href='/products/jamblaster' learn more
br clear="all"
br clear="all"
- content_for :after_black_bar do
.latest-promo
= render :partial => "latest"
.endorsement-promo
.home-buzz
h2 What Musicians in the JamKazam Community Are Saying
= link_to image_tag("web/thumbnail_buzz.jpg", :alt => "JamKazam Endorsements!", width:300), '#', class: "endorsements-video video-item", 'data-video-header' => 'JamKazam Community', 'data-video-url' => 'http://www.youtube.com/embed/_7qj5RXyHCo?autoplay=1'
br clear="all"
javascript:
window.JK.HomePage();

View File

@ -0,0 +1,12 @@
= provide(:title, 'Unsubscribe')
- if request.get?
%h2 Unsubscribe from all JamKazam email for address #{@user} ?
%br
= form_tag("") do
= submit_tag('Unsubscribe')
= hidden_field_tag(:user_token, params[:user_token])
- elsif request.post?
- if @user && ! @user.subscribe_email
%h2 You have been unsubscribed.

View File

@ -220,7 +220,7 @@ if defined?(Bundler)
# amount of time before we think the queue is stuck
config.signing_job_queue_max_time = 20 # 20 seconds
config.email_alerts_alias = 'nobody@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_alerts_alias = 'alerts@jamkazam.com' # should be used for 'oh no' server down/service down sorts of emails
config.email_generic_from = 'nobody@jamkazam.com'
config.email_smtp_address = 'smtp.sendgrid.net'
config.email_smtp_port = 587
@ -323,5 +323,6 @@ if defined?(Bundler)
config.one_free_jamtrack_per_user = true
config.nominated_jam_track = 'jamtrack-pearljam-alive'
config.recurly_tax_estimate_jam_track_plan = 'jamtrack-acdc-backinblack'
end
end

View File

@ -8,7 +8,7 @@ SampleApp::Application.routes.draw do
resources :users
resources :sessions, only: [:new, :create, :destroy]
root to: 'users#welcome'
root to: 'users#home'
# signup, and signup completed, related pages
match '/signup', to: 'users#new', :via => 'get'
@ -79,6 +79,8 @@ SampleApp::Application.routes.draw do
match '/reset_password_token' => 'users#reset_password_token', :via => :get
match '/reset_password_complete' => 'users#reset_password_complete', :via => :post
match '/unsubscribe/:user_token' => 'users#unsubscribe', via: [:get, :post]
# email update
match '/confirm_email' => 'users#finalize_update_email', :as => 'confirm_email' # NOTE: if you change this, you break outstanding email changes because links in user inboxes are broken
@ -276,6 +278,9 @@ SampleApp::Application.routes.draw do
match '/recurly/update_billing_info' => 'api_recurly#update_billing_info', :via => :put
match '/recurly/place_order' => 'api_recurly#place_order', :via => :post
# sale info
match '/sales' => 'api_sales#index', :via => :get
# login/logout
match '/auth_session' => 'api_users#auth_session_create', :via => :post
match '/auth_session' => 'api_users#auth_session_delete', :via => :delete

View File

@ -0,0 +1,57 @@
require 'spec_helper'
describe ApiSalesController do
render_views
let(:user) {FactoryGirl.create(:user)}
let(:jam_track) {FactoryGirl.create(:jam_track)}
before(:each) do
controller.current_user = user
end
describe "index" do
it "empty" do
get :index, { :format => 'json'}
response.should be_success
body = JSON.parse(response.body)
body['next_page'].should be_nil
body['entries'].should eq([])
end
it "one item" do
sale = Sale.create_jam_track_sale(user)
sale.recurly_invoice_id = SecureRandom.uuid
sale.save!
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
get :index, { :format => 'json'}
response.should be_success
body = JSON.parse(response.body)
body['next_page'].should be_nil
entries = body['entries']
entries.should have(1).items
sale_entry = entries[0]
sale_entry["line_items"].should have(1).items
sale_entry["recurly_transactions"].should have(0).items
transaction = FactoryGirl.create(:recurly_transaction_web_hook, invoice_id: sale.recurly_invoice_id, transaction_type: RecurlyTransactionWebHook::VOID)
get :index, { :format => 'json'}
response.should be_success
body = JSON.parse(response.body)
body['next_page'].should be_nil
entries = body['entries']
entries.should have(1).items
sale_entry = entries[0]
sale_entry["line_items"].should have(1).items
sale_entry["recurly_transactions"].should have(1).items
end
end
end

View File

@ -756,4 +756,25 @@ FactoryGirl.define do
bpm 120
tap_in_count 3
end
factory :recurly_transaction_web_hook, :class => JamRuby::RecurlyTransactionWebHook do
transaction_type JamRuby::RecurlyTransactionWebHook::SUCCESSFUL_PAYMENT
sequence(:recurly_transaction_id ) { |n| "recurly-transaction-id-#{n}" }
sequence(:subscription_id ) { |n| "subscription-id-#{n}" }
sequence(:invoice_id ) { |n| "invoice-id-#{n}" }
sequence(:invoice_number ) { |n| 1000 + n }
invoice_number_prefix nil
action 'purchase'
status 'success'
transaction_at Time.now
amount_in_cents 199
reference 100000
message 'meh'
association :user, factory: :user
factory :recurly_transaction_web_hook_failed do
transaction_type JamRuby::RecurlyTransactionWebHook::FAILED_PAYMENT
end
end
end

View File

@ -5,6 +5,7 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
subject { page }
let(:user) { FactoryGirl.create(:user) }
let(:jam_track) {FactoryGirl.create(:jam_track)}
before(:each) do
UserMailer.deliveries.clear
@ -135,6 +136,26 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
}
end
end
end
describe "payment history" do
it "show 1 sale" do
sale = Sale.create_jam_track_sale(user)
shopping_cart = ShoppingCart.create(user, jam_track)
sale_line_item = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, 'some_adjustment_uuid', nil)
visit "/client#/account"
find('.account-mid.payments', text: 'You have made 1 purchase.')
find("#account-payment-history-link").trigger(:click)
find('h2', text: 'payment history:')
find('table tr td', text: '$0.00') # 1st purchase is free
end
end

View File

@ -40,8 +40,6 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
# make sure plans are there
@recurlyClient.create_jam_track_plan(@jamtrack_acdc_backinblack) unless @recurlyClient.find_jam_track_plan(@jamtrack_acdc_backinblack)
@recurlyClient.create_jam_track_plan(@jamtrack_pearljam_evenflow) unless @recurlyClient.find_jam_track_plan(@jamtrack_pearljam_evenflow)
end
@ -552,7 +550,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
sale = user.sales.first
sale.sale_line_items.length.should eq(2)
acdc_sale = SaleLineItem.find_by_recurly_subscription_uuid(acdc.recurly_subscription_uuid)
acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(acdc.recurly_adjustment_uuid)
acdc_sale.recurly_plan_code.should eq(jamtrack_acdc_backinblack.plan_code)
acdc_sale.product_type.should eq('JamTrack')
acdc_sale.product_id.should eq(jamtrack_acdc_backinblack.id)
@ -560,7 +558,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
acdc_sale.free.should eq(0)
acdc_sale.unit_price.should eq(1.99)
acdc_sale.sale.should eq(sale)
pearljam_sale = SaleLineItem.find_by_recurly_subscription_uuid(pearljam.recurly_subscription_uuid)
pearljam_sale = SaleLineItem.find_by_recurly_adjustment_uuid(pearljam.recurly_adjustment_uuid)
pearljam_sale.recurly_plan_code.should eq(jamtrack_pearljam_evenflow.plan_code)
pearljam_sale.product_type.should eq('JamTrack')
pearljam_sale.product_id.should eq(jamtrack_pearljam_evenflow.id)
@ -660,7 +658,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
guy.sales.length.should eq(1)
sale = guy.sales.first
sale.sale_line_items.length.should eq(1)
acdc_sale = SaleLineItem.find_by_recurly_subscription_uuid(jam_track_right.recurly_subscription_uuid)
acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(jam_track_right.recurly_adjustment_uuid)
acdc_sale.recurly_plan_code.should eq(jamtrack_acdc_backinblack.plan_code)
acdc_sale.product_type.should eq('JamTrack')
acdc_sale.product_id.should eq(jamtrack_acdc_backinblack.id)
@ -710,7 +708,7 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d
guy.sales.length.should eq(2)
sale = guy.sales.last
sale.sale_line_items.length.should eq(1)
acdc_sale = SaleLineItem.find_by_recurly_subscription_uuid(jam_track_right.recurly_subscription_uuid)
acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(jam_track_right.recurly_adjustment_uuid)
acdc_sale.recurly_plan_code.should eq(jamtrack_pearljam_evenflow.plan_code)
acdc_sale.product_type.should eq('JamTrack')
acdc_sale.product_id.should eq(jamtrack_pearljam_evenflow.id)

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