code-complete minus a few features

This commit is contained in:
Seth Call 2020-11-29 18:24:28 -06:00
parent 64fdf3028a
commit cf00671123
50 changed files with 1734 additions and 333 deletions

View File

@ -78,7 +78,7 @@ gem 'slim'
#gem 'influxdb', '0.1.8'
#gem 'influxdb-rails', '0.1.10'
gem 'influxdb-rails'
gem 'recurly'
gem 'recurly', '~> 2'
gem 'sendgrid_toolkit', '>= 1.1.1'
gem 'stripe'
gem 'zip-codes'

View File

@ -465,7 +465,7 @@ GEM
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
recurly (2.12.1)
recurly (2.18.18)
redis (4.0.1)
redis-namespace (1.6.0)
redis (>= 3.0.4)
@ -665,7 +665,7 @@ DEPENDENCIES
rails (> 4.2)
rails-jquery-autocomplete
rails-observers
recurly
recurly (~> 2)
resque
resque-failed-job-mailer
resque-lonely_job (~> 1.0.0)

View File

@ -23,6 +23,41 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
end
#Notification.send_reload(connection.client_id)
member_action :give_free_plan, :method => :get do
@client = RecurlyClient.new
plan_code = params[:plan_code]
if params[:plan_code] == ''
plan_code = nil
end
resource.update_admin_override_plan_code(plan_code)
redirect_to :back, {notice: "User got a free plan via adminstrative override to #{params[:plan_code]}"}
end
member_action :revoke_free_plan, :method => :get do
resource.update_admin_override_plan_code(nil)
redirect_to :back, {notice: "User has administrative free plan removed"}
end
member_action :sync_subscription, :method => :get do
@client = RecurlyClient.new
@client.sync_subscription(resource)
redirect_to :back, {notice: "Check the Subscription Plan Code, Subscription Sync Code, Subscription Sync Msg"}
end
member_action :change_to_plan, :method => :get do
@client = RecurlyClient.new
plan_code = params[:plan_code]
if params[:plan_code] == ''
plan_code = nil
end
result, subscription, account = @client.update_desired_subscription(resource, plan_code)
redirect_to :back, {notice: "Set user's desired plan. "}
end
member_action :quick_reset, :method => :get do
resetting_to = 'jellyandjam123'
resource.change_password(resetting_to, resetting_to)
@ -35,6 +70,18 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
redirect_to :back, {notice: "Reset password url created: #{reset_url}"}
end
member_action :end_trial, :method => :get do
if DateTime.now - 2.days < resource.subscription_trial_ends_at
resource.subscription_trial_ends_at = 3.days.ago
resource.save!
redirect_to :back, {notice: "User's trial ended"}
else
redirect_to :back, {notice: "Users trial already ended > 2 days ago. Left alone user account"}
end
end
show do |user|
@ -89,7 +136,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
row "Quick Password Reset" do |user|
span do
link_to("Reset Password to jellyandjam123", quick_reset_admin_user_path(user.id), :data => {:confirm => 'Reset password to jellyandjam123 ?'})
link_to("reset password to jellyandjam123", quick_reset_admin_user_path(user.id), :data => {:confirm => 'Reset password to jellyandjam123 ?'})
end
end
@ -101,7 +148,106 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
row "Password Reset URL" do |user|
span do
link_to("Create Reset URL", create_reset_admin_user_path(user.id), :data => {:confirm => 'Are you sure?'})
link_to("create reset URL", create_reset_admin_user_path(user.id), :data => {:confirm => 'Are you sure?'})
end
end
row "Subscription" do |user|
div do
attributes_table do
row :desired_plan_code, 'hi'
row :subscription_plan_code
row :admin_override_plan_code
row :recurly_subscription_state
row :recurly_subscription_id
row :desired_plan_code_set_at
row :subscription_plan_code_set_at
row :subscription_last_checked_at
row :subscription_trial_ends_at
row :subscription_sync_code
row :subscription_sync_msg
row :is_past_due
row :stored_credit_card
end
end
div do
'DESIRED PLAN CODE = What plan the user has selected in the UI'
end
div do
'SUBSCIRPTION PLAN CODE = What plan the user actually has'
end
div do
div do
user.recurly_code ? link_to('Recurly Account', user.recurly_link_to_account, target: "_blank", ) : span do 'No Recurly Account' end
end
div do
user.recurly_subscription_id ? link_to('Recurly Subscription', user.recurly_link_to_subscription, target: "_blank", ) : span do 'No Recurly Subscription' end
end
end
div do
h3 do
'Give Free Plan Actions'
end
h4 do
'sets secret override to give user a free plan'
end
div do
link_to("give free silver plan", give_free_plan_admin_user_path(user.id, plan_code: 'jamsubsilver'), :data => {:confirm => 'Are you sure?'})
end
div do
link_to("give free gold plan", give_free_plan_admin_user_path(user.id, plan_code: 'jamsubgold'), :data => {:confirm => 'Are you sure?'})
end
div do
link_to("give free platinum plan", give_free_plan_admin_user_path(user.id, plan_code: 'jamsubplatinum'), :data => {:confirm => 'Are you sure?'})
end
div do
link_to("revoke free plan", revoke_free_plan_admin_user_path(user.id), :data => {:confirm => 'Are you sure?'})
end
end
div do
h3 do
'Change Plan Actions'
end
h4 do
'exactly as if the user did it in the UI'
end
div do
link_to("change plan to silver", change_to_plan_admin_user_path(user.id, 'jamsubsilver'), :data => {:confirm => 'Are you sure?'})
end
div do
link_to("change plan to gold", change_to_plan_admin_user_path(user.id, plan_code: 'jamsubgold'), :data => {:confirm => 'Are you sure?'})
end
div do
link_to("change plan to platinum", change_to_plan_admin_user_path(user.id, plan_code: 'jamsubplatinum'), :data => {:confirm => 'Are you sure?'})
end
div do
link_to("change plan to free", change_to_plan_admin_user_path(user.id, plan_code: ''), :data => {:confirm => 'Are you sure?'})
end
end
div do
h3 do
'Force Sync'
end
h4 do
'exactly same as background job that checks accounts'
end
div do
link_to("force sync", sync_subscription_admin_user_path(user.id), :data => {:confirm => 'Are you sure?'})
end
end
div do
h3 do
'End Trial'
end
h4 do
'ends the trial as of 3 days ago'
end
div do
link_to("end trial", end_trial_admin_user_path(user.id), :data => {:confirm => 'Are you sure?'})
end
end
end

View File

@ -86,6 +86,11 @@ module JamAdmin
config.external_protocol = ENV['EXTERNAL_PROTOCOL'] || 'http://'
config.external_root_url = "#{config.external_protocol}#{config.external_hostname}#{(config.external_port == 80 || config.external_port == 443) ? '' : ':' + config.external_port.to_s}"
config.recurly_root_url = 'https://jamkazam-development.recurly.com'
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb'
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
config.recurly_public_api_key = 'ewr1-HciusxMNfSSjz5WlupGk0C'
# where is rabbitmq?
config.rabbitmq_host = "127.0.0.1"
@ -144,9 +149,9 @@ module JamAdmin
config.max_track_part_upload_failures = 3
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
config.recurly_private_api_key = '7d623daabfc2434fa2a893bb008eb3e6'
config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb'
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
config.recurly_public_api_key = 'sc-SZlO11shkeA1WMGuISLGg5'
config.recurly_public_api_key = 'ewr1-HciusxMNfSSjz5WlupGk0C'
# these values work out of the box with default settings of an influx install (you do have to add a development database by hand though)
config.influxdb_database = "development"

View File

@ -41,6 +41,7 @@ JamAdmin::Application.configure do
# Show the logging configuration on STDOUT
config.show_log_configuration = true
config.recurly_subdomain = 'jamkazam-development'
config.email_support_alias = 'support-dev@jamkazam.com'
config.email_generic_from = 'nobody-dev@jamkazam.com'
config.email_alerts_alias = 'alerts-dev@jamkazam.com'

View File

@ -0,0 +1,9 @@
Recurly.api_key = Rails.configuration.recurly_private_api_key
Recurly.subdomain = Rails.configuration.recurly_subdomain
Recurly.default_currency = 'USD'
Recurly.logger = Rails.logger
Recurly::API.net_http = {
ssl_version: :TLSv1_2,
#...
}

View File

@ -97,28 +97,41 @@ ALTER TABLE users ADD COLUMN recurly_subscription_id VARCHAR(100) DEFAULT NULL;
ALTER TABLE users ADD COLUMN recurly_token VARCHAR(200) DEFAULT NULL;
ALTER TABLE users ADD COLUMN recurly_subscription_state VARCHAR(20) DEFAULT NULL;
ALTER TABLE users ADD COLUMN subscription_plan_code VARCHAR(100) DEFAULT NULL;
CREATE TABLE subscriptions (
id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(200) UNIQUE NOT NULL UNIQUE NOT NULL,
play_time_per_session_mins INT DEFAULT NULL,
play_time_per_month_mins INT DEFAULT NULL,
can_record BOOLEAN DEFAULT TRUE,
audio_max_bitrate INT DEFAULT NULL,
save_as_wave BOOLEAN DEFAULT FALSE,
pro_audio BOOLEAN DEFAULT FALSE,
video_resolution VARCHAR(50) DEFAULT NULL,
broadcasting_type VARCHAR(50) DEFAULT NULL,
music_lessons VARCHAR(50) DEFAULT NULL,
support VARCHAR(50) DEFAULT NULL,
max_players_per_session INT DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE users ADD COLUMN desired_plan_code VARCHAR(100) DEFAULT NULL;
ALTER TABLE users ADD COLUMN admin_override_plan_code VARCHAR(100) DEFAULT NULL;
ALTER TABLE users ADD COLUMN desired_plan_code_set_at TIMESTAMP;
ALTER TABLE users ADD COLUMN subscription_plan_code_set_at TIMESTAMP;
ALTER TABLE users ADD COLUMN subscription_last_checked_at TIMESTAMP;
ALTER TABLE users ADD COLUMN subscription_sync_code VARCHAR;
ALTER TABLE users ADD COLUMN subscription_sync_msg VARCHAR;
ALTER TABLE users ADD COLUMN client_fingerprint VARCHAR(255);
ALTER TABLE users ADD COLUMN is_past_due BOOLEAN DEFAULT FALSE;
CREATE INDEX subscription_sync_code_user_index ON users USING btree(subscription_sync_code);
ALTER TABLE users ADD COLUMN subscription_trial_ends_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE users ADD COLUMN subscription_plan_reason varchar(20);
UPDATE users set subscription_trial_ends_at = (CURRENT_TIMESTAMP + '30 days'::interval), subscription_plan_code = 'jamsubgold';
CREATE INDEX msuh_user_id ON music_sessions_user_history((1)) WHERE is_a_student;
-- CREATE TABLE subscriptions (
-- id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(),
-- name VARCHAR(200) UNIQUE NOT NULL UNIQUE NOT NULL,
-- play_time_per_session_mins INT DEFAULT NULL,
-- play_time_per_month_mins INT DEFAULT NULL,
-- can_record BOOLEAN DEFAULT TRUE,
-- audio_max_bitrate INT DEFAULT NULL,
-- save_as_wave BOOLEAN DEFAULT FALSE,
-- pro_audio BOOLEAN DEFAULT FALSE,
-- video_resolution VARCHAR(50) DEFAULT NULL,
-- broadcasting_type VARCHAR(50) DEFAULT NULL,
-- music_lessons VARCHAR(50) DEFAULT NULL,
-- support VARCHAR(50) DEFAULT NULL,
-- max_players_per_session INT DEFAULT NULL,
-- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-- );
--CREATE INDEX msuh_user_id ON music_sessions_user_history((1)) WHERE is_a_student;
-- alreday on WWW
CREATE INDEX msuh_user_id ON music_sessions_user_history USING btree (user_id);

View File

@ -70,9 +70,9 @@ CREATE OR REPLACE FUNCTION generate_scores_dataset () RETURNS VOID STRICT VOLATI
DELETE FROM jamisp;
INSERT INTO jamisp (beginip, endip, coid) SELECT x.beginip, x.endip, y.coid FROM geoipisp x, jamcompany y WHERE x.company = y.company;
UPDATE geoiplocations SET geog = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography;
UPDATE geoipblocks SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
UPDATE jamisp SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
--UPDATE geoiplocations SET geog = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography;
--UPDATE geoipblocks SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
--UPDATE jamisp SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
IF EXISTS(

View File

@ -4,8 +4,8 @@ CREATE SCHEMA tiger;
CREATE SCHEMA topology;
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public;
CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder WITH SCHEMA tiger;
--CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; -- don't use this anymore
--CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder WITH SCHEMA tiger; -- don't use this anymore
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--CREATE EXTENSION IF NOT EXISTS postgis_topology WITH SCHEMA topology;

View File

@ -106,19 +106,19 @@ DELETE FROM jamisp;
INSERT INTO jamisp (beginip, endip, coid) SELECT x.beginip, x.endip, y.coid FROM geoipisp x, jamcompany y WHERE x.company = y.company;
--ALTER TABLE geoiplocations DROP COLUMN geog;
ALTER TABLE geoiplocations ADD COLUMN geog geography(point, 4326);
UPDATE geoiplocations SET geog = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography;
CREATE INDEX geoiplocations_geog_gix ON geoiplocations USING GIST (geog);
--ALTER TABLE geoiplocations ADD COLUMN geog geography(point, 4326);
--UPDATE geoiplocations SET geog = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography;
--CREATE INDEX geoiplocations_geog_gix ON geoiplocations USING GIST (geog);
--ALTER TABLE geoipblocks DROP COLUMN geom;
ALTER TABLE geoipblocks ADD COLUMN geom geometry(polygon);
UPDATE geoipblocks SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
CREATE INDEX geoipblocks_geom_gix ON geoipblocks USING GIST (geom);
--ALTER TABLE geoipblocks ADD COLUMN geom geometry(polygon);
--UPDATE geoipblocks SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
--CREATE INDEX geoipblocks_geom_gix ON geoipblocks USING GIST (geom);
--ALTER TABLE jamisp DROP COLUMN geom;
ALTER TABLE jamisp ADD COLUMN geom geometry(polygon);
UPDATE jamisp SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
CREATE INDEX jamisp_geom_gix ON jamisp USING GIST (geom);
--ALTER TABLE jamisp ADD COLUMN geom geometry(polygon);
--UPDATE jamisp SET geom = ST_MakeEnvelope(beginip, -1, endip, 1);
--CREATE INDEX jamisp_geom_gix ON jamisp USING GIST (geom);
--DROP VIEW current_scores;
CREATE VIEW current_scores AS SELECT * FROM scores s WHERE score_dt = (SELECT max(score_dt) FROM scores s0 WHERE s0.alocidispid = s.alocidispid AND s0.blocidispid = s.blocidispid);

View File

@ -286,6 +286,7 @@ message LoginAck {
optional string username = 10;
optional int32 client_id_int = 11;
repeated Ars arses = 12;
optional SiteSubscription subscription = 13;
}
message ConnectAck {
@ -826,6 +827,20 @@ message ClientUpdate {
optional int32 size = 4;
}
message SiteSubscription {
optional int32 play_time_per_month = 1;
optional int32 play_time_per_session = 2;
optional bool can_record_audio = 3;
optional bool can_use_video = 4;
optional bool can_record_video = 5;
optional bool can_record_wave = 6;
optional int32 audio_max_bitrate = 7;
optional int32 video_resolution = 8;
optional bool can_broadcast = 9;
optional int32 broadcasting_type = 10;
optional int32 max_players = 11;
}
message Ars {
optional int32 id = 1;
optional string ip = 2;

View File

@ -84,7 +84,7 @@ module JamRuby
)
end
# create a login ack (login was successful)
def login_ack(public_ip, client_id, token, heartbeat_interval, music_session_id, reconnected, user_id, connection_expire_time, username, client_id_int, client_update_data = nil, arses = [])
def login_ack(public_ip, client_id, token, heartbeat_interval, music_session_id, reconnected, user_id, connection_expire_time, username, client_id_int, client_update_data = nil, arses = [], subscription=nil)
client_update = Jampb::ClientUpdate.new(
product: client_update_data[:product],
version: client_update_data[:version],
@ -105,7 +105,20 @@ module JamRuby
:client_update => client_update,
:username => username,
:client_id_int => client_id_int,
:arses => arses
:arses => arses,
:subscription => Jampb::SiteSubscription.new(
play_time_per_month: subscription[:play_time_per_month],
play_time_per_session: subscription[:play_time_per_session],
can_record_audio: subscription[:can_record_audio],
can_use_video: subscription[:can_use_video],
can_record_video: subscription[:can_record_video],
can_record_wave: subscription[:can_record_wave],
audio_max_bitrate: subscription[:audio_max_bitrate],
video_resolution: subscription[:video_resolution],
can_broadcast: subscription[:can_broadcast],
broadcasting_type: subscription[:broadcasting_type],
max_players: subscription[:max_players]
)
)
Jampb::ClientMessage.new(

View File

@ -274,11 +274,11 @@ module JamRuby
if sale.valid?
client = RecurlyClient.new
# this is handled in update_payment now
#account = client.find_or_create_account(current_user, nil, recurly_token)
#client.update_billing_info_from_token(current_user, account, recurly_token)
account = client.find_or_create_account(current_user, nil, recurly_token)
client.update_billing_info_from_token(current_user, account, recurly_token)
account = client.get_account(current_user)
if account.present?
recurly_response = client.create_subscription(current_user, plan_code, account)
current_user.recurly_subscription_id = recurly_response.uuid

View File

@ -381,9 +381,21 @@ module JamRuby
end
def self.hourly_check
send_onboarding_surveys
send_take_lesson_poke
first_lesson_instructions
#send_onboarding_surveys
#send_take_lesson_poke
#first_lesson_instructions
subscription_sync
end
def self.subscription_sync
recurly_client = RecurlyClient.new
User
.where('subscription_last_checked_at is NULL OR users.subs < ?', 1.days.ago)
.where("recurly_subscription_id IS NOT NULL OR (subscription_sync_code not in ('trial_ended', 'no_recurly_account', 'admin_control', 'school_license', 'no_subscription_or_expired'))")
.order('subscription_last_checked_at ASC NULLS FIRST')
.each do |user|
recurly_client.sync_subscription(user)
end
end
def self.first_lesson_instructions
@ -398,7 +410,6 @@ module JamRuby
User.where(id: user.id).update_all(sent_first_lesson_instr_email_at: Time.now)
end
end
def self.send_onboarding_surveys
@ -728,6 +739,10 @@ module JamRuby
license_end && Time.now > license_end
end
def has_active_license?
license_end && !license_expired?
end
def session_count
0
#MusicSession.where("user_id = ? AND started_at IS NOT NULL", self.id).size
@ -1449,6 +1464,11 @@ module JamRuby
license_start = options[:license_start]
license_end = options[:license_end]
import_source = options[:import_source]
desired_plan_code = options[:desired_plan_code]
if desired_plan_code == ''
desired_plan_code = nil
end
test_drive_package = TestDrivePackage.find_by_name(test_drive_package_details[:name]) if test_drive_package_details
@ -1498,6 +1518,10 @@ module JamRuby
user.retailer_interest = !!retailer_interest
user.school_interest = !!school_interest
user.education_interest = !!education_interest
user.desired_plan_code = desired_plan_code
user.subscription_plan_code = SubscriptionDefinitions::JAM_GOLD
user.desired_plan_code_set_at = DateTime.now
user.subscription_trial_ends_at = DateTime.now + 30.days
if user.is_a_student || user.is_a_teacher
musician = true
end
@ -2829,6 +2853,25 @@ module JamRuby
rules
end
def update_admin_override_plan_code(plan_code)
self.admin_override_plan_code = plan_code
self.subscription_plan_code = plan_code
self.subscription_plan_code_set_at = DateTime.now
self.save(validate: false)
end
def subscription_trial_ended?
subscription_trial_ends_at.nil? || DateTime.now > subscription_trial_ends_at
end
def recurly_link_to_account
"https://#{APP_CONFIG.recurly_subdomain}.recurly.com/accounts/#{id}"
end
def recurly_link_to_subscription
"https://#{APP_CONFIG.recurly_subdomain}.recurly.com/subscriptions/#{recurly_subscription_id}"
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64

View File

@ -44,14 +44,68 @@ module JamRuby
account
end
def update_desired_subscription(current_user, plan_code)
subscription = nil
account = nil
current_user.desired_plan_code = plan_code
current_user.desired_plan_code_set_at = DateTime.now
current_user.save(validate: false)
puts "updating desired subscription for #{current_user.email} to #{plan_code}"
account = get_account(current_user)
if account
if plan_code.nil? || plan_code == ''
begin
# user wants a free subscription. If they have a subscription, let's cancel it.
subscription, account = find_subscription(current_user, account)
if subscription
puts "Canceling user's #{current_user.email} subscription"
subscription.cancel
# do not delete the recurly_subscription_id ; we'll use that to try and reactivate later if they user re-activates their account
else
# if no subscription and past trial, you goin down -- because there must have never been payment??
if current_user.subscription_trial_ended?
current_user.subscription_plan_code = nil
current_user.subscription_plan_code_set_at = DateTime.now
current_user.save(validate: false)
end
end
# do not set the subscription _plan_code either; because the user has paid through the month; they still
# get their old plan
#current_user.subscription_plan_code = nil
#current_user.save(validate: false)
rescue => e
puts "Could not cancel subscription for user #{current_user.email}. #{e}"
return false, subscription, account
end
else
# user wants to pay. let's get it goin
return handle_create_subscription(current_user, plan_code, account)
end
end
return true, subscription, account
end
def get_account(current_user)
account = current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil
# check again, assuming account_code is the user ID (can happen in error scenarios where we create the account
# on recurly, but couldn't save the account_code to the user.recurly_code field)
puts "get_account for #{current_user.email} found #{account}"
if !account
account = Recurly::Account.find(current_user.id)
begin
account = Recurly::Account.find(current_user.id)
rescue Recurly::Error => x
puts "Swallow find acct error #{x}"
end
# repair user local account info
if !account.nil?
current_user.update_attribute(:recurly_code, account.account_code)
@ -78,39 +132,92 @@ module JamRuby
account
end
def payment_history(current_user, options ={})
def list_invoices(account)
invoices = []
count = 0
account.invoices.find_each do |invoice|
count = count + 1
invoices << invoice
if count == 50
break
end
end
invoices
end
def payment_history(current_user, params ={})
limit = params[:limit]
limit ||= 20
limit = limit.to_i
cursor = options[:cursor]
cursor = params[:cursor]
payments = []
account = get_account(current_user)
if(account.present?)
begin
account.transaction.paginate(per_page:limit, cursor:cursor).each do |transaction|
account.transactions.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 << {
:created_at => transaction.created_at,
:amount_in_cents => transaction.amount_in_cents,
:tax_in_cents=> transaction.tax_in_cents,
:status => transaction.status,
:action => transaction.action,
:payment_method => transaction.payment_method,
:reference => transaction.reference,
:plan_code => transaction.plan_code
:currency => transaction.currency
}
#end
end
rescue Recurly::Error, NoMethodError => x
puts "Recurly error #{current_user.email} #{x}"
raise RecurlyClientError, x.to_s
end
end
payments
end
def invoice_history(current_user, params ={})
limit = params[:limit]
limit ||= 20
limit = limit.to_i
cursor = params[:cursor]
payments = []
account = get_account(current_user)
if(account.present?)
begin
account.invoices.paginate(per_page:limit, cursor:cursor).each do |invoice|
# 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 << {
:created_at => invoice.created_at,
:subtotal_in_cents => invoice.subtotal_in_cents,
:tax_in_cents=> invoice.tax_in_cents,
:total_in_cents => invoice.total_in_cents,
:state => invoice.state,
:description => invoice.line_items.map(&:description).join(", "),
:currency => invoice.currency
}
#end
end
rescue Recurly::Error, NoMethodError => x
puts "Recurly error #{current_user.email} #{x}"
raise RecurlyClientError, x.to_s
end
end
return payments, account
end
def update_billing_info(current_user, billing_info=nil, account = nil)
account = get_account(current_user) if account.nil?
if account.present?
@ -211,51 +318,124 @@ module JamRuby
raise RecurlyClientError.new(plan.errors) if plan.errors.any?
end
def handle_create_subscription(current_user, plan_code, account)
begin
subscription = create_subscription(current_user, plan_code, account, current_user.subscription_trial_ended? ? nil : current_user.subscription_trial_ends_at)
current_user.recurly_subscription_id = subscription.uuid
if current_user.subscription_trial_ended?
current_user.subscription_plan_code = plan_code
current_user.subscription_plan_code_set_at = DateTime.now
else
# we could force a platinum plan since the user has put forward payment already, even in trial
puts "user #{current_user.email} is in trial"
if plan_code == SubscriptionDefinitions::JAM_PLATINUM || plan_code == SubscriptionDefinitions::JAM_PLATINUM_YEARLY
puts "user #{current_user.email} is in trial and buying platinum ; upgrade them already"
current_user.subscription_plan_code = plan_code
current_user.subscription_plan_code_set_at = DateTime.now
else
current_user.subscription_plan_code = SubscriptionDefinitions::JAM_GOLD
current_user.subscription_plan_code_set_at = DateTime.now
end
end
current_user.save(validate: false)
rescue => e
puts "Could not create subscription for user #{current_user.email}. #{e}"
return false, subscription, account
end
return true, subscription, account
end
# https://dev.recurly.com/docs/create-subscription
def create_subscription(user, plan_code, account)
puts "Creating subscription for #{user.email} with plan_code #{plan_code}"
subscription = Recurly::Subscription.create(
:plan_code => plan_code,
:currency => 'USD',
:customer_notes => 'Thank you for your business!',
:account => {
:account_code => account.account_code
},
:auto_renew => true
)
def create_subscription(user, plan_code, account, starts_at = nil)
old_subscription_id = user.recurly_subscription_id
if old_subscription_id
# first, let's try to reactivate it
old_subscription = Recurly::Subscription.find(old_subscription_id)
begin
old_subscription.reactivate
puts "reactivated plan! Let's check if it needs changing"
if plan_code != old_subscription.plan.plan_code
result = old_subscription.update_attributes(
:plan_code => plan_code,
:timeframe => starts_at.nil? ? 'bill_date' : 'now'
)
end
return old_subscription
rescue => e
puts "Unable to reactivate/update old plan #{e}"
user.update_attribute(:recurly_subscription_id, nil)
end
end
if account.billing_info
puts "Creating subscription for #{user.email} with plan_code #{plan_code}"
subscription = Recurly::Subscription.create(
:plan_code => plan_code,
:currency => 'USD',
:customer_notes => 'Thank you for your business!',
:account => {
:account_code => account.account_code
},
:starts_at => starts_at,
:auto_renew => true
)
subscription
else
puts "User has no billing info; not trying to create a subscription #{user.email}"
end
subscription
end
def find_subscription(user, account = nil)
def find_subscription(user, fed_account = nil)
subscription = nil
account = nil
if fed_account.nil?
account = get_account(user)
else
account = fed_account
end
if user.recurly_subscription_id.nil?
if account.nil?
account = get_account(user)
end
if account
active_subscription = nil
account.subscriptions.find_each do |subscription|
puts "Subscription: #{subscription.inspect}"
puts "Subscription: #{subscription.inspect} #{subscription.state}"
if subscription.state == :active || subscription.state == :future
active_subscription = subscription
break
end
end
subscription = account.subscriptions.first
subscription = active_subscription
else
puts "can't find subscription for account #{account}"
end
else
subscription = Recurly::Subscription.find(user.recurly_subscription_id)
begin
subscription = Recurly::Subscription.find(user.recurly_subscription_id)
rescue Recurly::Resource::NotFound
puts "subscription is gone. delete it!"
user.update_attribute(:recurly_subscription_id, nil)
user.recurly_subscription_id = nil
end
end
if user.recurly_subscription_id.nil?
if subscription && user.recurly_subscription_id.nil?
puts "Repairing subscription ID on account"
user.update_attribute(:recurly_subscription_id, subscription.id)
user.recurly_subscription_id = subscription.id
end
subscription
return [subscription, account]
end
def change_subscription_plan(current_user, plan_code)
subscription = find_subscription(current_user)
subscription, account = find_subscription(current_user)
if subscription.nil?
puts "no subscription found for user #{current_user.email}"
@ -279,20 +459,133 @@ module JamRuby
def sync_subscription(user)
subscription = find_subscription(user)
if subscription.nil?
if user.subscription_plan_code
begin
# edge case: admin controlled
if user.admin_override_plan_code
puts "admin controlled plan #{user.email}"
user.subscription_plan_code = user.admin_override_plan_code
user.subscription_plan_code_set_at = DateTime.now
user.subscription_last_checked_at = DateTime.now
user.subscription_sync_code = 'admin_control'
user.subscription_sync_msg = "admin override - plan_code set to #{user.admin_override_plan_code}"
user.save(validate: false)
return
end
# edge case: user is in a licensed school
if user.has_active_license?
puts "user has school license #{user.email}"
user.subscription_plan_code = SubscriptionDefinitions::JAM_PLATINUM
user.subscription_plan_code_set_at = DateTime.now
user.subscription_last_checked_at = DateTime.now
user.subscription_sync_code = 'school_license'
user.subscription_sync_msg = "has school license - plan_code set to #{SubscriptionDefinitions::JAM_PLATINUM}"
user.save(validate: false)
return
end
# if user is in trial still, not much book-keeping
if !user.subscription_trial_ended?
puts "user has a trial still #{user.email}"
# there is actually nothing to do, because we don't start billing for any plan until trial is over.
user.subscription_last_checked_at = DateTime.now
user.subscription_sync_code = 'in_trial'
user.subscription_sync_msg = "trial still active - plan_code not altered"
user.save(validate: false)
return
end
# if the trial has ended, but it was within 2 days of ending the trial date, give the user some leeway
if user.subscription_trial_ended? && (user.subscription_trial_ends_at.nil? || (DateTime.now < ( user.subscription_trial_ends_at + 2.days )))
puts "user recently ended trial; ignore them #{user.email}"
user.subscription_last_checked_at = DateTime.now
user.subscription_sync_code = 'trial_recently_ended'
user.subscription_sync_msg = "trial ended in past 2 days - plan_code not altered"
user.save(validate: false)
return
end
# if there is no recurly action here, then they must be coming off of a trial and we have to mark them down
if user.recurly_code.nil? && !user.subscription_plan_code.nil?
puts "new user #{user.email} has no payment info and is ending their trial"
# TODO: send email
user.subscription_plan_code = nil
user.subscription_plan_code_set_at = DateTime.now
user.subscription_last_checked_at = DateTime.now
user.subscription_sync_code = 'trial_ended'
user.subscription_sync_msg = "trial ended and no subscription set - plan_code set to Free"
user.save(validate: false)
return
end
account = get_account(user)
if account.nil?
puts "Account is nil? #{user.email}. Strange"
user.subscription_last_checked_at = DateTime.now
user.save(validate: false)
user.subscription_sync_code = 'no_recurly_account'
user.subscription_sync_msg = "user has no recurly account - plan_code not altered"
return
end
user.is_past_due = account.has_past_due_invoice
subscription, account = find_subscription(user, account)
if subscription
user.recurly_subscription_state = subscription.state
else
user.recurly_subscription_state = nil
user.save(validate:false)
end
else
user.recurly_subscription_state = subscription.state
if user.subscription_plan_code != subscription.plan.plan_code
user.subscription_plan_code = subscription.plan.plan_code
if subscription.nil? || subscription.state == 'expired'
puts "user has expired or no plan"
user.subscription_plan_code = nil
user.subscription_plan_code_set_at = DateTime.now
user.subscription_sync_code = 'no_subscription_or_expired'
user.subscription_sync_msg = "user has no or expired subscription - plan_code set to Free"
else
if user.is_past_due
if !user.subscription_plan_code.nil?
puts "user #{user.email} has a past due plan. We gotta bring them down"
user.subscription_plan_code = nil
user.subscription_plan_code_set_at = DateTime.now
user.subscription_sync_code = 'is_past_due_changed'
user.subscription_sync_msg = "payment has gone past due - plan_code set to Free"
else
puts "user is past due and #{user.email} had no changes"
user.subscription_sync_code = 'is_past_due_unchanged'
user.subscription_sync_msg = "payment has gone past due, plan_code not altered because already set to free"
end
else
if user.subscription_plan_code != user.desired_plan_code
puts "they are back! get them back into their desired plan #{user.email}"
user.subscription_plan_code = user.desired_plan_code
user.subscription_plan_code_set_at = DateTime.now
user.subscription_sync_code = 'good_standing_repaired'
user.subscription_sync_msg = "user is in good standing but desired != effective; plan_code set to #{user.desired_plan_code}"
else
puts "good standing user #{user.email} had no changes"
user.subscription_sync_code = 'good_standing_unchanged'
user.subscription_sync_msg = "user is in good standing but already set correctly; plan_code not altered"
end
end
end
user.subscription_last_checked_at = DateTime.now
user.save(validate: false)
rescue => e
puts "Unexpected error in sync_subscription for user #{user.email}"
puts e.message
user.subscription_last_checked_at = DateTime.now
user.subscription_sync_code = 'failed_sync'
user.subscription_sync_msg = e.message
user.save(validate: false)
end
end
def find_or_create_account(current_user, billing_info, recurly_token = nil)

View File

@ -1,11 +1,11 @@
module JamRuby
class SubscriptionDefinitions
JAM_SILVER = 'jamsubsilver'
JAM_SILVER_WITH_TRIAL = 'jamsubsilvertrial'
JAM_SILVER_YEARLY = 'jamsubsilveryearly'
JAM_GOLD = 'jamsubgold'
JAM_GOLD_WITH_TRIAL = 'jamsubgoldtrial'
JAM_GOLD_YEARLY = 'jamsubgoldyearly'
JAM_PLATINUM = 'jamsubplatinum'
JAM_PLATINUM_WITH_TRIAL = 'jamsubplatinumtrial'
JAM_PLATINUM_YEARLY = 'jamsubplatinumyearly'
# ALL IN HOURS
FREE_PLAY_TIME_PER_SESSION = 1
@ -21,10 +21,14 @@ module JamRuby
FREE_PLAN = {
play_time_per_month: FREE_PLAY_TIME_PER_MONTH,
play_time_per_session: FREE_PLAY_TIME_PER_SESSION,
recording: false,
video: 'no',
audio_bitrate: '128',
broadcasting: 'no',
can_record_audio: false,
can_use_video: false,
can_record_video: false,
can_record_wave: false,
video_resolution: 0,
audio_max_bitrate: 1, # 128
can_broadcast: false,
broadcasting_type: 3,
max_players: 4
}
@ -32,41 +36,53 @@ module JamRuby
SILVER_PLAN = {
play_time_per_month: SILVER_PLAY_TIME_PER_MONTH,
play_time_per_session: SILVER_PLAY_TIME_PER_SESSION,
recording: false,
video: 'cif',
audio_bitrate: '192',
broadcasting: 'free',
can_record_audio: false,
can_record_video: false,
can_use_video: true,
can_record_wave: true,
video_resolution: 1, # CIF in the backend
audio_max_bitrate: 2, #192
can_broadcast: true,
broadcasting_type: 3,
max_players: 6
}
GOLD_PLAN = {
play_time_per_month: GOLD_PLAY_TIME_PER_MONTH,
play_time_per_session: GOLD_PLAY_TIME_PER_SESSION,
recording: true,
video: '720p',
audio_bitrate: '256',
broadcasting: 'free',
can_record_audio: true,
can_record_video: true,
can_use_video: true,
can_record_wave: true,
video_resolution: 3, # 720p in the backend
audio_max_bitrate: 3, #256
can_broadcast: true,
broadcasting_type: 3,
max_players: nil
}
PLATINUM_PLAN = {
play_time_per_month: PLATINUM_PLAY_TIME_PER_MONTH,
play_time_per_session: PLATINUM_PLAY_TIME_PER_SESSION,
recording: true,
video: '1080p',
audio_bitrate: '512',
broadcasting: 'busking',
can_record_audio: true,
can_record_video: true,
can_use_video: true,
can_record_wave: true,
video_resolution: 4, # 1080p in the backend
audio_max_bitrate: 5, #512
can_broadcast: true,
broadcasting_type: 3,
max_players: nil
}
def self.rules(plan_code)
if plan_code == nil
FREE_PLAN
elsif plan_code == JAM_SILVER || plan_code == JAM_SILVER_WITH_TRIAL
elsif plan_code == JAM_SILVER || plan_code == JAM_SILVER_YEARLY
SILVER_PLAN
elsif plan_code == JAM_GOLD || plan_code == JAM_GOLD_WITH_TRIAL
elsif plan_code == JAM_GOLD || plan_code == JAM_GOLD_YEARLY
GOLD_PLAN
elsif plan_code == JAM_PLATINUM || plan_code == JAM_PLATINUM_WITH_TRIAL
elsif plan_code == JAM_PLATINUM || plan_code == JAM_PLATINUM_YEARLY
PLATINUM_PLAN
else
raise "unknown plan #{plan_code}"

View File

@ -0,0 +1,46 @@
require 'spec_helper'
describe "User Subscriptions" do
let(:user1) {FactoryGirl.create(:user)}
let(:client) { RecurlyClient.new }
before(:each) do
end
it "empty results" do
user1.touch
User.subscription_sync
user1.reload
user1.subscription_sync_code.should be_nil
user1.subscription_last_checked_at.should be_nil
end
it "user not in trial" do
user1.subscription_plan_code = SubscriptionDefinitions::JAM_PLATINUM
user1.subscription_trial_ends_at = 1.days.ago
client.sync_subscription(user1)
user1.reload
user1.subscription_sync_code.should == "trial_recently_ended"
user1.subscription_last_checked_at.should_not be_nil
user1.subscription_plan_code.should == SubscriptionDefinitions::JAM_PLATINUM
user1.subscription_trial_ends_at = 3.days.ago
user1.subscription_last_checked_at = 2.days.ago
user1.save!
User.subscription_sync
user1.reload
user1.subscription_sync_code.should == "trial_ended"
user1.subscription_last_checked_at.should_not be_nil
user1.subscription_plan_code.should be_nil
end
end

View File

@ -680,16 +680,31 @@
// browser: use session cookie, and auth with token
// native: use session cookie, and use the token
// latency_tester: ask for client ID from backend; no token (trusted)
if (isClientMode()) {
var client_type = context.JK.clientType()
var client_id = (gon.global.env == "development" ? $.cookie('client_id') : null)
var machine = context.jamClient.SessionGetMacHash()
if (machine) {
machine = machine.all
}
}
else {
var client_type = 'latency_tester'
var client_id = context.jamClient.clientID
var machine = null
}
var params = {
channel_id: channelId,
token: rememberToken,
client_type: isClientMode() ? context.JK.clientType() : 'latency_tester',
client_id: isClientMode() ? (gon.global.env == "development" ? $.cookie('client_id') : null): context.jamClient.clientID,
client_type: client_type, // isClientMode() ? context.JK.clientType() : 'latency_tester',
client_id: client_id,
machine: machine,
os: context.JK.GetOSAsString(),
//jamblaster_serial_no: context.PlatformStore.jamblasterSerialNo(),
udp_reachable: context.JK.StunInstance ? !context.JK.StunInstance.sync() : null // latency tester doesn't have the stun class loaded
}
var uri = context.gon.websocket_gateway_uri + '?' + $.param(params); // Set in index.html.erb.
logger.debug("connecting websocket: " + uri);

View File

@ -51,6 +51,9 @@
return null;
}
function applySubscriptionPolicy() {
}
function OpenSystemBrowser(href) {
dbg("OpenSystemBrowser('" + href + "')");
context.window.open(href);
@ -1378,6 +1381,10 @@
function UserAttention(option) {
}
function SessionGetMacHash() {
return null;
}
function IsFrontendVisible() {
return true;
}
@ -1733,6 +1740,7 @@
// Websocket/Auth sessions
this.OnLoggedIn = OnLoggedIn;
this.OnLoggedOut = OnLoggedOut;
this.SessionGetMacHash = SessionGetMacHash;
this.UserAttention = UserAttention;
this.IsFrontendVisible = IsFrontendVisible;
@ -1801,6 +1809,7 @@
this.getPluginList = getPluginList;
this.clearPluginList = clearPluginList;
this.listTrackAssignments = listTrackAssignments;
this.applySubscriptionPolicy = applySubscriptionPolicy;
this.clientID = "devtester";
};

View File

@ -2813,6 +2813,17 @@
})
}
function updatePayment(options) {
options = options || {}
return $.ajax({
type: "POST",
url: '/api/recurly/update_payment',
dataType: "json",
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function getSubscription() {
return $.ajax({
type: "GET",
@ -2822,8 +2833,8 @@
})
}
function changeSubscription(options) {
options = options || {}
function changeSubscription(plan_code) {
var options = {plan_code: plan_code}
return $.ajax({
type: "POST",
url: '/api/recurly/change_subscription',
@ -2844,6 +2855,15 @@
})
}
function listInvoices(options) {
return $.ajax({
type: "GET",
url: '/api/recurly/invoice_history',
dataType: "json",
contentType: 'application/json'
})
}
function createLiveStream(musicSessionId) {
return $.ajax({
type: "POST",
@ -3171,9 +3191,11 @@
this.findPublicSessions = findPublicSessions;
this.getConfigClient = getConfigClient;
this.createSubscription = createSubscription;
this.updatePayment = updatePayment;
this.getSubscription = getSubscription;
this.changeSubscription = changeSubscription;
this.cancelSubscription= cancelSubscription;
this.listInvoices = listInvoices;
return this;
};
})(window, jQuery);

View File

@ -27,6 +27,8 @@
var value = $(this).val()
enable_disable_instruments(value == "true")
})
// jam_ruby_user[desired_plan_code]
}
// register form elements relating to location to update appropriately as the user makes changes

View File

@ -1,9 +1,11 @@
context = window
rest = context.JK.Rest()
logger = context.JK.logger
LocationActions = context.LocationActions
AppStore = context.AppStore
UserStore = context.UserStore
SubscriptionActions = context.SubscriptionActions
profileUtils = context.JK.ProfileUtils
@ -12,7 +14,9 @@ profileUtils = context.JK.ProfileUtils
mixins: [
ICheckMixin,
Reflux.listenTo(AppStore, "onAppInit"),
Reflux.listenTo(UserStore, "onUserChanged")
Reflux.listenTo(UserStore, "onUserChanged"),
Reflux.listenTo(@LocationStore, "onLocationsChanged"),
Reflux.listenTo(SubscriptionStore, "onSubscriptionChanged")
]
shownOnce: false
@ -47,25 +51,26 @@ profileUtils = context.JK.ProfileUtils
componentDidMount: () ->
@checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}]
#@checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}]
@root = $(@getDOMNode())
@endOfList = @root.find('.end-of-payments-list')
@contentBodyScroller = @root
@iCheckify()
#@iCheckify()
@preparePaymentMethod()
componentDidUpdate: (prevProps, prevState) ->
@iCheckify()
#@iCheckify()
$expiration = @root.find('input.expiration')
if !$expiration.data('payment-applied')
$expiration.payment('formatCardExpiry').data('payment-applied', true)
$cardNumber = @root.find("input.card-number")
if !$cardNumber.data('payment-applied')
$cardNumber.payment('formatCardNumber').data('payment-applied', true)
$cvv = @root.find("input.cvv")
if !$cvv.data('payment-applied')
$cvv.payment('formatCardCVC').data('payment-applied', true)
#$expiration = @root.find('input.expiration')
#if !$expiration.data('payment-applied')
# $expiration.payment('formatCardExpiry').data('payment-applied', true)
#$cardNumber = @root.find("input.card-number")
#if !$cardNumber.data('payment-applied')
# $cardNumber.payment('formatCardNumber').data('payment-applied', true)
#$cvv = @root.find("input.cvv")
#if !$cvv.data('payment-applied')
# $cvv.payment('formatCardCVC').data('payment-applied', true)
if @currentNext() == null
@contentBodyScroller.off('scroll')
@ -81,6 +86,8 @@ profileUtils = context.JK.ProfileUtils
if @activeTile(prevState.selected) != @activeTile() && @getCurrentList().length == 0
@refresh()
@configureElements()
registerInfiniteScroll:() ->
$scroller = @contentBodyScroller
@ -119,10 +126,10 @@ profileUtils = context.JK.ProfileUtils
@clearResults()
@screenVisible = true
@refresh()
@getUncollectables()
#@getUncollectables()
resetErrors: () ->
@setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null})
@setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null, firstNameError: null, lastNameError: null, cityError: null, stateError: null, address1Error: null, countryError: null, address2Error:null})
onBack: (e) ->
e.preventDefault()
@ -140,14 +147,24 @@ profileUtils = context.JK.ProfileUtils
@refreshTeacherDistributions()
else if @activeTile() == @TILE_PAYMENTS_TO_JAMKAZAM
@refreshSales()
else if @activeTile() == @TILE_PAYMENT_METHOD
@refreshPayment()
else
logger.debug("dropping refresh because no tile match", @activeTile)
refreshPayment:() ->
SubscriptionActions.updateSubscription()
refreshSales: () ->
@refreshing = true
rest.getSalesHistory(@currentQuery)
.done(@salesHistoryDone)
.fail(@salesHistoryFail)
#rest.getSalesHistory(@currentQuery)
#.done(@salesHistoryDone)
#.fail(@salesHistoryFail)
rest.listInvoices()
.done(@paymentHistoryDone)
.fail(@paymentHistoryFail)
refreshTeacherDistributions: () ->
@refreshing = true
@ -168,6 +185,14 @@ profileUtils = context.JK.ProfileUtils
@refreshing = false
@app.notifyServerError jqXHR, 'Payments to JamKazam Unavailable'
paymentHistoryDone:(response) ->
@refreshing = false
this.setState({salesNext: null, sales: this.state.sales.concat(response.entries), pastDue: response.past_due})
paymentHistoryFail:(jqXHR) ->
@refreshing = false
@app.notifyServerError jqXHR, 'Payments to JamKazam Unavailable'
teacherDistributionsDone:(response) ->
@refreshing = false
this.setState({distributionsNext: response.next, distributions: this.state.distributions.concat(response.entries)})
@ -183,7 +208,7 @@ profileUtils = context.JK.ProfileUtils
@app.notifyServerError jqXHR, 'Unable to fetch uncollectable info'
clearResults:() ->
this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: 0, distributions: [], salesNext: null, distributionsNext: null, updating: false})
this.setState({salesCurrentPage: 0, sales: [], distributionsCurrentPage: 0, distributions: [], salesNext: null, distributionsNext: null, updating: false, pastDue: false})
buildQuery:(page = @getCurrentPage()) ->
@currentQuery = this.defaultQuery(page)
@ -245,10 +270,11 @@ profileUtils = context.JK.ProfileUtils
distributionsNext: null
sales: [],
distributions: []
selected: 'payments to jamkazam',
selected: @TILE_PAYMENT_METHOD,
updating: false,
billingInUS: true,
userWantsUpdateCC: false,
selectedCountry: null,
uncollectables: []
}
@ -334,6 +360,154 @@ profileUtils = context.JK.ProfileUtils
<br className="clearall" />
</div>`
onSubscriptionChanged: (subscription) ->
@setState({pastDue: subscription.past_due})
onLocationsChanged: (countries) ->
console.log("countries in ", countries)
@setState({countries: countries})
onCountryChanged: (e) ->
val = $(e.target).val()
@setState({selectedCountry: val})
currentCountry: () ->
this.state.selectedCountry || this.props.selectedCountry || ''
openBrowser: () ->
context.JK.popExternalLink("https://www.jamkazam.com/client#/subscription")
onRecurlyToken: (err, token_data) ->
if err
console.log("error", err)
handled = false
if err.code == "validation" && err.fields?
handled = true
for field in err.fields
console.log("problem field", field)
# handle error using err.code and err.fields
if field == "first_name"
@setState({firstNameError: true})
if field == "last_name"
@setState({lastNameError: true})
if err.code == "invalid-parameter"
if err.fields.indexOf("year") > -1 || err.fields.indexOf("month") > -1 || err.fields.indexOf("number") > -1 || err.fields.indexOf("cvv") > -1
@setState({ccError: true})
handled = true
@app.layout.notify({title: 'Please double-check ' + err.fields[0], text: err.message})
if !handled
@app.layout.notify({title: "Error Updating Payment Info", text: JSON.stringify(err)})
@setState({updating: false})
else
# recurly.js has filled in the 'token' field, so now we can submit the
# form to your server
console.log("eintercepted", token_data)
rest.updatePayment({recurly_token: token_data.id}).done((response) => @updatePaymentDone(response)).fail((jqXHR) => @updatePaymentFailure(jqXHR))
updatePaymentDone: (response) ->
@setState({updating: false})
logger.debug("recurly submitted: " + JSON.stringify(response))
@setState({userWantsUpdateCC: false})
#if @state.shouldShowName
window.UserActions.refresh()
if response.uncollectables
context.JK.Banner.showAlert('Credit Card Updated', 'Than you. Your credit card info has been updated.<br/><br/>We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.')
else
@app.layout.notify({title: 'Payment Updated', text: 'Your payment info has been updated.'})
updatePaymentFailure: (jqXHR) ->
@setState({updating: false})
handled = false
if jqXHR.status == 404
errors = JSON.parse(jqXHR.responseText)?.message
@app.layout.notify({title: "Error Updating Payment Info", text: errors})
else
@app.notifyServerError(jqXHR, 'Payment Not Updated')
onFormSubmit: (event) ->
form = event.target
console.log("ok work this", form)
event.preventDefault()
@setState({updating: true})
recurly.token(@elements, form, @onRecurlyToken)
configureRecurly: () ->
unless window.configuredRecurly
console.log("configuring recurly...")
window.recurly.configure(gon.global.recurly_public_api_key)
window.configuredRecurly = true
@elements = window.recurly.Elements()
delayedConfigure: () ->
if !window.recurly?
console.log("relaunch delayed recurly configure")
setTimeout(() =>
@delayedConfigure()
, 1000)
return
@configureRecurly()
@configureElements()
configureElements: () ->
node = $('#subscription-elements')
if node.length > 0
if window.recurly && @elements? && !node.data('recurlied')
commonStyles = {
inputType: 'mobileSelect',
style: {
fontSize:'1em'
color:'black',
placeholder: {
color:'black',
}
invalid: {
fontColor: 'red'
}
}
}
#cardNumberStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'Card Number'}}})
#cardMonthStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'MM'}}})
#cardYearStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'YYYY'}}})
#cardCvcStyle = $.extend(true, {}, commonStyles, {style:{placeholder:{content: 'CVC'}}})
#cardNumberElement = @elements.CardNumberElement( cardNumberStyle )
#cardMonthElement = @elements.CardMonthElement(cardMonthStyle)
#cardYearElement = @elements.CardYearElement(cardYearStyle)
#cardCvvElement = @elements.CardCvvElement(cardCvcStyle)
#cardNumberElement.attach('#subscription-elements-number')
#cardMonthElement.attach('#subscription-elements-month')
#cardYearElement.attach('#subscription-elements-year')
#cardCvvElement.attach('#subscription-elements-cvv')
cardElement = @elements.CardElement(commonStyles)
cardElement.attach("#subscription-elements")
document.querySelector('#user-payment-submit').addEventListener('submit', @onSubmit.bind(this))
node.data('recurlied', true)
preparePaymentMethod: () ->
LocationActions.load()
setTimeout(() =>
@delayedConfigure()
, 200)
defaultText: () ->
'Select Country'
paymentMethod: () ->
disabled = @state.updating || @reuseStoredCard()
@ -347,14 +521,37 @@ profileUtils = context.JK.ProfileUtils
inUSClasses = {field: true, "billing-in-us": true, error: @state.billingInUSError}
zipCodeClasses = {field: true, "zip-code": true, error: @state.zipCodeError}
nameClasses= {field: true, "name": true, error: @state.nameError}
firstNameClasses= {field: true, "first-name": true, error: @state.firstNameError}
lastNameClasses= {field: true, "last-name": true, error: @state.lastNameError}
address1Classes= {field: true, "address-1": true, error: @state.address1Error}
address2Classes= {field: true, "address-2": true, error: @state.address2Error}
cityClasses= {field: true, "city": true, error: @state.cityError}
stateClasses= {field: true, "state": true, error: @state.stateError}
countryClasses = {field: true, "country": true, error: @state.countryError}
formClasses= {stored: @reuseStoredCard()}
leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()}
rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()}
if @state.countries?
countries = [`<option key="" value="">{this.defaultText()}</option>`]
for countryId, countryInfo of @state.countries
countries.push(`<option key={countryId} value={countryId}>{countryInfo.name}</option>`)
country = @state.countries[this.currentCountry()]
else
countries = []
countryJsx = `
<select disabled={disabled} name="countries" onChange={this.onCountryChanged} value={this.currentCountry()} data-recurly="country" autoComplete="shipping country" className="country">{countries}</select>`
if @state.uncollectables.length > 0
uncollectable = @state.uncollectables[0]
uncollectableMessage = `<div className="uncollectable-msg">A charge for your music lesson with {uncollectable.teacher.name} failed. Please update your credit card information immediately so that we can pay the instructor. If you have called your credit card provider and believe there should be no problem with your card, please email us at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> so that we can figure out what's gone wrong. Thank you!</div>`
if @state.pastDue
uncollectableMessage = `<div className="uncollectable-msg">A charge for your subscription has failed. Please update your credit card information immediately so that you can restore your plan.</div>`
if @state.user?['has_stored_credit_card?'] && @state.uncollectables.length == 0
if @state.userWantsUpdateCC
header = 'Please update your billing address and payment information below.'
@ -362,60 +559,73 @@ profileUtils = context.JK.ProfileUtils
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
<a className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
<a className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION</a>
</div>`
else
header = 'You have already entered a credit card in JamKazam.'
header = 'You have have a payment method on file already.'
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
managedSubscriptionAction = `<a href="/client#/account/subscription" className="button-orange">MANAGE MY SUBSCRIPTION</a>`
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
{managedSubscriptionAction}
</div>`
else
header = 'Please enter your billing address and payment information below.'
header = `<div>Please enter your billing address and payment information below.<br/><br/><span class="no-storage-note">This information is sent to <a href="https://recurly.com/customers/" rel="external">Recurly</a>, which is compliant with the PCI-DSS security standard. JamKazam does not receive or store your credit card.</span></div>`
actions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a><a
className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION</a>
</div>`
firstNameField =
`<div className={classNames(firstNameClasses)}>
<label>First Name:</label>
<input disabled={disabled} type="text" name="first_name" className="first_name" data-recurly="first_name" required autoComplete="cc-given-name"></input>
</div>`
lastNameField =
`<div className={classNames(lastNameClasses)}>
<label>Last Name:</label>
<input disabled={disabled} type="text" name="last_name" className="last_name" data-recurly="last_name" required autoComplete="cc-surname"></input>
</div>`
if @state.shouldShowName && @state.user?.name?
username = @state.user?.name
nameField =
`<div className={classNames(nameClasses)}>
<label>Name:</label>
<input id="set-user-on-card" disabled={disabled} type="text" name="name" className="name" defaultValue={username}></input>
</div>`
`<div>
<div className={classNames(leftColumnClasses)}>
{uncollectableMessage}
<div className="paymethod-header">{header}</div>
<form autoComplete="on" onSubmit={this.onSubmit} className={classNames(formClasses)}>
{nameField}
<div className={classNames(cardNumberFieldClasses)}>
<label>Card Number:</label>
<input placeholder="1234 5678 9123 4567" type="tel" autoComplete="cc-number" disabled={disabled}
type="text" name="card-number" className="card-number"></input>
<form autoComplete="on" onSubmit={this.onSubmit} className={classNames(formClasses)} id="user-payment-submit">
{firstNameField}
{lastNameField}
<div className={classNames(address1Classes)}>
<label>Address 1:</label>
<input type="text" data-recurly="address1" disabled={disabled} required autoComplete="shipping address-line1" className="address-1"></input>
</div>
<div className={classNames(expirationFieldClasses)}>
<label>Expiration Date:</label>
<input placeholder="MM / YY" autoComplete="cc-expiry" disabled={disabled} type="text" name="expiration"
className="expiration"></input>
<div className={classNames(address2Classes)}>
<label>Address 2:</label>
<input type="text" data-recurly="address2" disabled={disabled} autoComplete="shipping address-line2" className="address-2"></input>
</div>
<div className={classNames(cvvFieldClasses)}>
<label>CVV:</label>
<input autoComplete="off" disabled={disabled} type="text" name="cvv" className="cvv"></input>
<div className={classNames(cityClasses)}>
<label for="city">City:</label>
<input type="text" data-recurly="city" className="city" disabled={disabled} required autoComplete="shipping address-level2"></input>
</div>
<div className={classNames(stateClasses)}>
<label for="state">State:</label>
<input type="text" data-recurly="state" className="state" disabled={disabled} required autoComplete="shipping address-level1"></input>
</div>
<div className={classNames(zipCodeClasses)}>
<label>Zip Code</label>
<input autoComplete="off" disabled={disabled || !this.state.billingInUS} type="text" name="zip"
<label>Postal Code:</label>
<input type="text" name="zip" data-recurly="postal_code" disabled={disabled} required autoComplete="shipping postal-code"
className="zip"></input>
</div>
<div className={classNames(inUSClasses)}>
<label>Billing Address<br/>is in the U.S.</label>
<input type="checkbox" name="billing-address-in-us" className="billing-address-in-us"
value={this.state.billingInUS}/>
<div className={classNames(countryClasses)}>
<label for="country">Country:</label>
{countryJsx}
</div>
<input style={{'display':'none'}} type="submit" name="submit"/>
<div className={classNames(cardNumberFieldClasses)}>
<label>Card:</label>
<span className="recurly-holder" id="subscription-elements"></span>
</div>
<input type="hidden" name="recurly-token" data-recurly="token"></input>
</form>
{actions}
</div>
@ -458,7 +668,6 @@ profileUtils = context.JK.ProfileUtils
<thead>
<tr>
<th>CHARGED AT</th>
<th>METHOD</th>
<th>DESCRIPTION</th>
<th>REASON</th>
<th>AMOUNT</th>
@ -484,19 +693,24 @@ profileUtils = context.JK.ProfileUtils
description = items.join(', ')
else
# this is a recurly webhook
transaction = paymentHistory.transaction
amt = transaction.amount_in_cents
status = transaction.transaction_type
#transaction = paymentHistory.transaction
#amt = transaction.amount_in_cents
#status = transaction.transaction_type
#displayAmount = '($' + (amt/100).toFixed(2) + ')'
#date = context.JK.formatDate(transaction.transaction_at, true)
#description = transaction.admin_description
invoice = paymentHistory
amt = invoice.total_in_cents
status = invoice.state
displayAmount = '($' + (amt/100).toFixed(2) + ')'
date = context.JK.formatDate(transaction.transaction_at, true)
description = transaction.admin_description
date = context.JK.formatDate(invoice.created_at, true)
description = invoice.description
amountClasses = {status: status}
row =
`<tr>
<td>{date}</td>
<td className="capitalize">{paymentMethod}</td>
<td>{description}</td>
<td className="capitalize">{status}</td>
<td className={classNames(amountClasses)}>{displayAmount}</td>
@ -509,7 +723,6 @@ profileUtils = context.JK.ProfileUtils
<thead>
<tr>
<th>DATE</th>
<th>METHOD</th>
<th>DESCRIPTION</th>
<th>STATUS</th>
<th>AMOUNT</th>
@ -530,7 +743,7 @@ profileUtils = context.JK.ProfileUtils
selectionMade: (selection, e) ->
e.preventDefault()
@getUncollectables()
#@getUncollectables()
@setState({selected: selection})
activeTile: (selected = this.state.selected) ->
@ -594,7 +807,7 @@ profileUtils = context.JK.ProfileUtils
`<div className="content-body-scroller">
<div className="profile-header profile-head">
<div className="account-header">payment<br/>history:</div>
<div className="account-header">payment<br/>management:</div>
{profileNav}
<div className="clearall"></div>
</div>
@ -621,7 +834,7 @@ profileUtils = context.JK.ProfileUtils
scheduling_communication = 'teacher'
correspondence_email = @root.find('input[name="correspondence_email"]').val()
@setState(updating: true)
@setState({updating: true})
rest.updateSchool({
id: this.state.school.id,
name: name,
@ -634,6 +847,52 @@ profileUtils = context.JK.ProfileUtils
@app.layout.notify({title: "update success", text: "Your school information has been successfully updated"})
onSubmitForm: (e) ->
form = document.querySelector('#user-payment-submit')
e.preventDefault()
@onSubmit(form)
onSubmit: (form) ->
@resetErrors()
#e.preventDefault()
console.log("onSubmit")
if !window.recurly?
@app.layout.notify({
title: 'Payment System Not Loaded',
text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!"
})
else
errored = false
# do a client-side sweep 1st
if !@root.find('input.first_name').val()
errored = true
@setState({firstNameError: true})
if !@root.find('input.last_name').val()
errored = true
@setState({lastNameError: true})
if !@root.find('input.address-1').val()
errored = true
@setState({address1Error: true})
if !@root.find('input.city').val()
errored = true
@setState({cityError: true})
if !@root.find('input.state').val()
errored = true
@setState({stateError: true})
if !@root.find('select.country').val()
errored = true
@setState({countryError: true})
if !@root.find('input.zip').val()
errored = true
@setState({zipCodeError: true})
if errored
return
#form = event.target
@setState({updating: true})
window.recurly.token(@elements, form, @onRecurlyToken)
onUpdateFail: (jqXHR) ->
handled = false
@ -643,8 +902,10 @@ profileUtils = context.JK.ProfileUtils
errors = JSON.parse(jqXHR.responseText)
handled = true
@setState({updateErrors: errors})
else
console.log("error path not taken", jqXHR)
onSubmit: (e) ->
onSubmitOld: (e) ->
@resetErrors()
e.preventDefault()
@ -761,8 +1022,8 @@ profileUtils = context.JK.ProfileUtils
#if @state.shouldShowName
window.UserActions.refresh()
if response.uncollectables
context.JK.Banner.showAlert('Credit Card Updated', 'Than you. Your credit card info has been updated.<br/><br/>We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.')
if response.past_due
context.JK.Banner.showAlert('Credit Card Updated', 'Thank you. Your credit card info has been updated.<br/><br/>We will try to bill any unpaid lessons within the next hour, and an email will be sent at that time.')
else
@app.layout.notify({title: 'Credit Card Updated', text: 'Your credit card info has been updated.'})

View File

@ -49,17 +49,12 @@ profileUtils = context.JK.ProfileUtils
if @state.subscription
if @state.subscription.plan
currentSubscription = `<CurrentSubscription subscription={this.state.subscription} />`
currentSubscription = `<CurrentSubscription subscription={this.state.subscription} app={this.app}/>`
createSubscription = `<Subscription subscription={this.state.subscription}/>`
content = `<div>
<div className="current-subscription-block">
{currentSubscription}
</div>
<div className="payment-block">
{createSubscription}
</div>
</div>`
else
content = `<div className="loading">Loading...</div>`
@ -72,9 +67,6 @@ profileUtils = context.JK.ProfileUtils
<div className="profile-wrapper">
<div className="main-content">
{content}
<div className="actions">
<a onClick={this.onCancel}>LEAVE</a>
</div>
<br />
</div>
</div>

View File

@ -10,90 +10,202 @@ AppStore = context.AppStore
mixins: [Reflux.listenTo(AppStore, "onAppInit")]
getInitialState: () ->
{
selectedPlan: null
updating: false
}
getDisplayNameTier: (plan_code) ->
if plan_code == ''
plan_code = null
for subscriptionCode in gon.global.subscription_codes
if plan_code == subscriptionCode.id
return subscriptionCode.name
return "Unknown plan code=#{plan_code}"
getDisplayNamePrice: (plan_code) ->
if plan_code == ''
plan_code = null
for subscriptionCode in gon.global.subscription_codes
if plan_code == subscriptionCode.id
return subscriptionCode.price
return "Unknown plan code=#{plan_code}"
onPlanChanged: (e) ->
val = $(e.target).val()
val = e.target.value
@setState({selectedPlan: val})
currentPlan: () ->
this.state.selectedPlan || this.props.selectedPlan || ''
if this.state.selectedPlan?
this.state.selectedPlan
else
this.props.subscription.desired_plan_code || ''
onChangeSubmit: (event) ->
form = event.target
event.preventDefault()
onSubmit: (event) ->
if event
event.preventDefault()
@performSubmit()
if !@state.selectedPlan
onChangeSubmit: (form) ->
if @state.updating
return
SubscriptionActions.changeSubscription(this.state.selectedPlan)
@performSubmit()
performSubmit: () ->
if !@state.selectedPlan?
return
@setState({updating: true})
#SubscriptionActions.changeSubscription(this.state.selectedPlan)
rest.changeSubscription(this.state.selectedPlan).done((subscription) =>
SubscriptionActions.forceUpdate(subscription)
has_billing_info = subscription.has_billing_info
console.log("subscription change update", subscription)
if has_billing_info
@props.app.layout.notify({
title: "Subscription updated!",
text: "Thank you for supporting JamKazam!"
})
else
@props.app.layout.notify({
title: "Payment method still needed",
text: "Please click UPDATE PAYMENT METHOD in the bottom-right of the screen."
})
@setState({updating: false, selectedPlan: null})
)
.fail((jqXHR) =>
@setState({updating: false})
if jqXHR.status == 422
@props.app.layout.notify({
title: "you already have this subscription",
text: "No changes were made to your account."
})
else
@props.app.layout.notify({
title: "unable to update subscription status",
text: "Please contact support@jamkazam.com. Error:\n #{jqXHR.responseText}"
})
)
onCancelPlan: (event) ->
if confirm("Are you sure you want to cancel your plan?")
SubscriptionActions.cancelSubscription()
componentDidUpdate: () ->
if @state.updating
@setState({updating:false})
componentDidMount: () ->
@root = $(@getDOMNode())
document.querySelector('#change-subscription-form').addEventListener('submit', @onChangeSubmit.bind(this))
comparePlans: (e) ->
if context.JK.clientType() == 'client'
context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
e.preventDefault()
render: () ->
plan_codes = []
monthlies = []
yearlies = []
for plan in gon.global.subscription_codes
plan_codes.push(`<option key={plan.id} value={plan.id}>{plan.name} ({plan.price}/month)</option>`)
if plan.cycle == 'month'
monthlies.push(plan)
else
yearlies.push(plan)
plan_codes.push(`<option key="monthly" value="monthly" disabled="disabled">-------- MONTHLY PLANS --------</option>`)
for plan in monthlies
plan_codes.push(`<option key={plan.id || ''} value={plan.id || ''}>{plan.name} (${plan.price.toFixed(2)}/{plan.cycle})</option>`)
plan_codes.push(`<option key="yearly" value="yearly" disabled="disabled">-------- YEARLY PLANS --------</option>`)
for plan in yearlies
plan_codes.push(`<option key={plan.id || ''} value={plan.id || ''}>{plan.name} (${plan.price.toFixed(2)}/{plan.cycle})</option>`)
plansJsx = `<select name="plan_code" onChange={this.onPlanChanged} defaultValue={this.currentPlan()} >{plan_codes}</select>`
plansJsx = `
<select name="plan_code" onChange={this.onPlanChanged} value={this.currentPlan()} >{plan_codes}</select>`
changeClass = 'button-orange'
if !@state.selectedPlan
changeClass = 'button-orange update-plan'
if !@state.selectedPlan? || @state.updating
changeClass = changeClass + ' disabled'
if @props.subscription.pending_subscription
currentPlan = this.getDisplayNameTier(this.props.subscription.pending_subscription.plan.plan_code)
billingAddendum = `<span>(billed at {this.getDisplayNameTier(this.props.subscription.plan.plan_code)} until next billing cycle</span>`
recurly_subscription = @props.subscription.subscription
effective_plan_name = `<span className="plan-name">{this.getDisplayNameTier(this.props.subscription.plan_code)}</span>`
desired_plan_name = `<span className="plan-name">{this.getDisplayNameTier(this.props.subscription.desired_plan_code)}</span>`
admin_override_plan_name = `<span className="plan-name">{this.getDisplayNameTier(this.props.subscription.admin_override_plan_code)}</span>`
in_trial = @props.subscription.in_trial
effective_is_free = !!this.props.subscription.plan_code
has_pending_subscription = @props.subscription.subscription?.pending_subscription?
cancelled_subscription = @props.subscription.subscription?.remaining_billing_cycles == 0
show_payment_info = null
has_billing_info = @props.subscription.has_billing_info
#console.log("@props.subscription.subscription", @props.subscription.subscription, has_pending_subscription)
if in_trial
if @props.subscription.desired_plan_code
if has_billing_info
note = `<span>Billing starts for the {desired_plan_name} plan after the trial ends.</span>`
else
warning = `<p className="uncollectable-msg">You will drop to the <span className="plan-name">Free</span> free plan after the trial ends because you have not yet entered payment info.</p>`
show_payment_info = true
else
if has_billing_info
warning = `<p className="uncollectable-msg">You will drop to the <span className="plan-name">Free</span> free plan after the trial ends because you have not selected a plan.</p>`
else
warning = `<p className="uncollectable-msg">You will drop to the <span className="plan-name">Free</span> free plan after the trial ends because you have not yet entered payment info or selected a plan.</p>`
show_payment_info = true
explanation = `<span>You have a <span className="plan-name">Gold</span> account until your trial ends {context.JK.formatDateShort(this.props.subscription.trial_ends_at)}. {note}</span>`
else
if @props.subscription.desired_plan_code && !@props.subscription.plan_code && !has_billing_info
explanation = `<span>You have successfully upgraded your plan to the {desired_plan_name} level, thank you!</span>`
warning = `<p className="uncollectable-msg">For this plan to take effect, you must provide a payment method (e.g. a credit card), for the monthly subscription charge. Please click the Update Payment Method button to do this now.</p>`
show_payment_info = true
else
if @props.subscription.desired_plan_code
if !has_billing_info
show_payment_info = true
explanation = `<span>You have successfully upgraded your plan to the {desired_plan_name} level, thank you</span>`
warning = `<p className="uncollectable-msg">However, you must provide a payment method (e.g. a credit card), for the monthly subscription charge. Please click the Update Payment Method button to do this now.</p>`
else
explanation = `<span>You have successfully upgraded your plan to the {desired_plan_name} level, thank you!</span>`
else
# free plan situation - not much to go on about
explanation = `<span>You are currently on the {desired_plan_name} plan.</span>`
if show_payment_info
update_payment_btn = `<a className="button-orange update-payment-method" href="/client#/account/paymentHistory">UPDATE PAYMENT METHOD</a>`
if has_pending_subscription
billingAddendum = null #`<span><br/><br/><span>You will be billed next at the <span className="plan-name">{this.getDisplayNameTier(this.props.subscription.subscription.plan.plan_code)}</span> on the next billing cycle.</span></span>`
else if cancelled_subscription && this.props.subscription.desired_plan_code == null && this.props.subscription.plan_code != null
billingAddendum = `<span>However, your cancelled {effective_plan_name} plan is still active until the end of the billing cycle.</span>`# `<span><br/><br/><span>You will be billed a final time at the <span className="plan-name">{this.getDisplayNameTier(this.props.subscription.subscription.plan.plan_code)}</span> at end of this billing cycle.</span></span>`
else
currentPlan = this.getDisplayNameTier(this.props.subscription.plan.plan_code)
billingAddendum = null
`<div>
`<div className="current-subscription">
<div>
<h3>Current Subscription</h3>
<div>
{currentPlan}
{billingAddendum}
<h2>Subscription:</h2>
<p className="explainer">Your JamKazam subscription is currently set to the plan displayed below. To compare the features available for different subscription plans, click the Compare Plans button below. To change your plan, click the "subscription plan" list box below, select a new plan, and then click the Update Plan button below.</p>
<div className="subscription-plan-and-status">
<div className="subscription-plan">
<form id="change-subscription-form" onSubmit={this.onChangeSubmit} >
<label for="plan_code">subscription plan:</label>
<div className="subscription-actions">
{plansJsx}
<a className="button-grey" onClick={this.comparePlans} target="_blank" href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-">COMPARE PLANS</a>
<button className={changeClass} onClick={this.onSubmit} type="submit">UPDATE PLAN</button>
</div>
</form>
</div>
</div>
<div className="effective-subscription">
<div className="explanation">
<p>{explanation} {billingAddendum}</p>
{warning}
</div>
<a className="button-grey" onClick={this.onCancel}>BACK</a>{update_payment_btn}
</div>
</div>
<div>
<h3>Change Plan</h3>
<form id="change-subscription-form">
<label for="plan_code">Change Plan To:</label>
{plansJsx}
<button className={changeClass}>CHANGE PLAN</button>
</form>
</div>
<div>
<h3>Cancel Plan</h3>
<button className="button-orange" onClick={this.onCancelPlan}>CANCEL PLAN</button>
</div>
</div>`
})

View File

@ -15,7 +15,6 @@ logger = context.JK.logger
{selectedCountry: null, countries: LocationStore.countries || {US: {name: 'United States', regions: []}}}
onLocationsChanged: (countries) ->
console.log("countires in ", countries)
@setState({countries: countries})
onCountryChanged: (e) ->

View File

@ -12,9 +12,31 @@ RecordingActions = @RecordingActions
getInitialState: () ->
{childWindow: null, isRecording: false}
openBrowserToPayment: () ->
context.JK.popExternalLink("/client#/account/subscription", true)
openBrowserToPlanComparison: () ->
context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
return 'noclose'
openRecording: () ->
RecordingActions.openRecordingControls()
canRecord = window.SessionStore.canRecord()
if canRecord
openRecordingControls()
else
buttons = []
buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (@openBrowserToPlanComparison()))})
buttons.push({
name: 'UPGRADE PLAN',
buttonStyle: 'button-orange',
click: (() => (@openBrowserToPayment()))
})
context.JK.Banner.show({
title: "Your Current Plan Does Not Allow Recording",
html: context._.template($('#template-plan-no-record').html(), {}, { variable: 'data' }),
buttons: buttons})
render: () ->
`<a className="session-record button-grey left" data-is-recording={this.state.isRecording} onClick={this.openRecording}>

View File

@ -3,10 +3,33 @@ SessionActions = @SessionActions
@SessionVideoBtn = React.createClass({
openBrowserToPayment: () ->
context.JK.popExternalLink("/client#/account/subscription", true)
openBrowserToPlanComparison: () ->
context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
return 'noclose'
sessionWebCam: (e) ->
e.preventDefault();
SessionActions.toggleSessionVideo()
canVideo = window.SessionStore.canRecord()
if canVideo
SessionActions.toggleSessionVideo()
else
buttons = []
buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (@openBrowserToPlanComparison()))})
buttons.push({
name: 'UPGRADE PLAN',
buttonStyle: 'button-orange',
click: (() => (@openBrowserToPayment()))
})
context.JK.Banner.show({
title: "Your Current Plan Does Not Allow Video",
html: context._.template($('#template-plan-no-video').html(), {}, { variable: 'data' }),
buttons: buttons})
render: () ->
`<a className="session-share button-grey-toggle left" onClick={this.sessionWebCam}>

View File

@ -58,7 +58,7 @@ UserStore = context.UserStore
configureRecurly: () =>
unless window.configuredRecurly
context.recurly.configure(gon.global.recurly_public_api_key)
window.recurly.configure(gon.global.recurly_public_api_key)
window.configuredRecurly = true
@ -87,6 +87,7 @@ UserStore = context.UserStore
}
}
)
console.log("attaching", $('#subscription-elements'))
cardElement.attach('#subscription-elements')
@root = $(@getDOMNode())
@ -132,7 +133,7 @@ UserStore = context.UserStore
<input type="text" data-recurly="address1" required autocomplete="shipping address-line1"></input>
<label for="address2">Address 2:</label>
<input type="text" data-recurly="address2" required autocomplete="shipping address-line1"></input>
<input type="text" data-recurly="address2" required autocomplete="shipping address-line2"></input>
<label for="city">City:</label>
<input type="text" data-recurly="city" required autocomplete="shipping address-level2"></input>
@ -145,10 +146,11 @@ UserStore = context.UserStore
<label for="country">Country:</label>
{countryJsx}
<label for="plan_code">Plan:</label>
{plansJsx}
</div>
<div id="subscription-elements">
<div id="subscription-elements-legacy">
</div>
<input type="hidden" name="recurly-token" data-recurly="token"></input>

View File

@ -44,33 +44,33 @@ context = window
openBrowserToPayment: (e) ->
context.JK.popExternalLink("/client#/account/subscription", true)
e.stopPropagation();
e.preventDefault();
openBrowserToPlanComparison: (e) ->
context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
e.stopPropagation();
e.preventDefault();
return 'noclose'
render: () ->
content = null
if @props.subscription.until.total < 2000
if @props.subscription.until.total < 60000
content = `<div className="message">
<p>You have run out of time.</p>
<p>You can <a href="/client#/account/subscription" onClick={this.openBrowserToPayment}>upgrade your plan</a> to continue playing.</p>
<p><a href="#" onClick={this.openBrowserToPlanComparison}>Compare plans</a> and consider <a href="/client#/account/subscription" onClick={this.openBrowserToPayment}>upgrading your plan</a> for more play time and premium features.</p>
</div>`
else
if @props.subscription.main_concern_type == 'remaining_session_play_time'
content = `<div className="message">
<p>You will run out play time for this session in:</p>
<p className="time">{this.displayTime()}</p>
<p>You can <a href="/client#/account/subscription" onClick={this.openBrowserToPayment}>upgrade your plan</a> to continue playing.</p>
<p><a href="#" onClick={this.openBrowserToPlanComparison}>Compare plans</a> and consider <a href="/client#/account/subscription" onClick={this.openBrowserToPayment}>upgrading your plan</a> for more play time and premium features.</p>
</div>`
else
content = `<div className="message">
<p>You will run out of monthly play time in:</p>
<p className="time">{this.displayTime()}</p>
<p>You can <a href="/client#/account/subscription" onClick={this.openBrowserToPayment}>upgrade your plan</a> to continue playing.</p>
<p><a href="#" onClick={this.openBrowserToPlanComparison}>Compare plans</a> and consider <a href="/client#/account/subscription" onClick={this.openBrowserToPayment}>upgrading your plan</a> for more play time and premium features.</p>
</div>`
if content?

View File

@ -5,4 +5,5 @@ context = window
changeSubscription: {}
cancelSubscription: {}
updatePayment: {}
forceUpdate: {}
})

View File

@ -47,7 +47,7 @@ BroadcastStore = Reflux.createStore(
subscriptionManagement: () ->
@subscriptionConcern.until = @lessonUtils.getTimeRemaining(@subscriptionConcern.main_concern_time)
if @subscriptionConcern.until.total < -15000
if @subscriptionConcern.until.total < -30000
leaveBehavior =
location: "/client#/findSession"
buttons = []

View File

@ -13,7 +13,6 @@ logger = context.JK.logger
this.listenTo(context.AppStore, this.onAppInit)
changed: () ->
console.log("changed", @countries)
@trigger(@countries)
onSelectCountry: (country) ->

View File

@ -1103,8 +1103,12 @@ ConfigureTracksActions = @ConfigureTracksActions
@sessionRules.remaining_session_until = until_time
if sessionData.subscription_rules
@subscriptionRules = sessionData.subscription_rules
if sessionData.subscription
# for the backend - it looks here
#sessionData.subscription = sessionData.subscription_rules
# let the backend know
#context.jamClient.applySubscriptionPolicy()
@subscriptionRules = sessionData.subscription
# TESTING:
#@subscriptionRules.remaining_month_play_time = 60 * 30 + 15 # 30 minutes and 15 seconds
@ -1114,9 +1118,7 @@ ConfigureTracksActions = @ConfigureTracksActions
#until_time.setSeconds(until_time.getSeconds() + @subscriptionRules.remaining_month_play_time)
@subscriptionRules.remaining_month_until = until_time
@currentSession = sessionData
if context.jamClient.UpdateSessionInfo?
if @currentSession?
context.jamClient.UpdateSessionInfo(@currentSession)
@ -1290,6 +1292,22 @@ ConfigureTracksActions = @ConfigureTracksActions
id: () ->
@currentSessionId
canRecord: () ->
if @subscriptionRules?
console.log("can record? rules:", @subscriptionRules.can_record_audio )
return @subscriptionRules.can_record_audio
else
console.log("can record? no rules; allow")
return true
canVideo: () ->
if @subscriptionRules?
console.log("can video? rules:", @subscriptionRules.can_use_video)
return @subscriptionRules.can_use_video
else
conssole.log("can video? no rules; allow")
return true
getCurrentOrLastSession: () ->
@currentOrLastSession

View File

@ -16,39 +16,21 @@ logger = context.JK.logger
onUpdateSubscription: (subscription) ->
console.log("refreshing subscription info")
rest.getSubscription().done (subscription) =>
@subscription = subscription
console.log("subscription store update", subscription)
@trigger(@subscription)
.fail(() =>
@app.layout.notify({
title: "unable to fetch subscription status",
title: "Unable to fetch subscription status",
text: "Please contact support@jamkazam.com"
})
)
onChangeSubscription: (plan_code) ->
rest.changeSubscription({plan_code: plan_code}).done((subscription) =>
@subscription = subscription
console.log("subscription change update", subscription)
@app.layout.notify({
title: "Subscription updated!",
text: "Thank you for supporting JamKazam!"
})
@trigger(@subscription)
)
.fail((jqXHR) =>
if jqXHR.status == 422
@app.layout.notify({
title: "you already have this subscription",
text: "No changes were made to your account."
})
else
@app.layout.notify({
title: "unable to update subscription status",
text: "Please contact support@jamkazam.com. Error:\n #{jqXHR.responseText}"
})
)
onForceUpdate: (subscription) ->
@subscription = subscription
@trigger(@subscription)
onCancelSubscription: () ->
rest.cancelSubscription().done((result) =>

View File

@ -4,11 +4,16 @@ logger = context.JK.logger
reactContext = if window.opener? then window.opener else window
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
if window.opener?
console.log("WINDOW.opener", window.opener)
try
m = window.opener.MixerStore
if !m?
console.log("it's still not set")
reactContext = window
catch e
reactContext = window
userAgent = window.navigator.userAgent;
if /iPhone|iPad|iPod|android/i.test(navigator.userAgent)
# iPad or iPhone

View File

@ -1,5 +1,48 @@
@import 'common.scss';
.recurly-element{
background-color:#c5c5c5 !important;
position: relative;
#width: 100%;
height: 25px;
#border: 2px solid black;
//background: transparent;
margin: 0 0 10px;
outline: none;
#font-family: 'Open Sans', Helvetica, sans-serif;
font-size: 1rem;
font-weight: bold;
box-shadow: none;
border-radius: 0;
color: black;
-webkit-appearance: none;
-webkit-transition: border-color 0.3s;
-moz-transition: border-color 0.3s;
-ms-transition: border-color 0.3s;
-o-transition: border-color 0.3s;
transition: border-color 0.3s;
display:inline-block;
width: calc(100% - 150px);
@include border_box_sizing;
max-width:400px;
padding-left:6px;
vertical-align: text-top;
}
.recurly-element-month {
width:40px;
text-align:center;
}
.recurly-element-year {
width:60px;
text-align:center;
}
.recurly-element-cvv {
width:60px;
text-align:center;
}
#account-payment-history {
.content-body-scroller {
@ -139,22 +182,54 @@
}
}
input {
.recurly-element {
height:25px;
}
#subscription-elements-month, #subscription-elements-cvv {
margin-left: 20px;
}
//.recurly-element {
// background-color:white;
//}
input, select {
display:inline-block;
width: calc(100% - 150px);
@include border_box_sizing;
max-width:200px;
font-family: Helvetica, serif;
font-weight:normal;
font-size:16px;
border-radius:0;
}
.field {
position:relative;
display:block;
margin-top:15px;
margin-bottom:25px;
margin-bottom:10px;
label {
width:150px;
}
}
.error {
background-color: transparent;
padding: 0;
border: solid 0px #900;
label {
color:red;
font-weight:bold;
}
input, select {
background-color: #c5c5c5 !important;
margin-bottom: 0;
}
}
.uncollectable-msg {
background-color:black;

View File

@ -2,6 +2,7 @@
#account-subscription {
.button-orange {margin:0;}
div[data-react-class="AccountSubscriptionScreen"] {
height: 100%;
}
@ -29,12 +30,19 @@
}
.uncollectable-msg {
background-color:black;
color:white;
padding:20px;
margin:20px 0;
}
#change-subscription-form {
max-width:35rem;
display:grid;
align-items: center;
#display:grid;
#align-items: center;
#justify-content: center;
grid-template-columns: 8rem 8rem 8rem;
#grid-template-columns: 8rem 8rem 8rem;
}
.payment-block {
@ -77,7 +85,6 @@
outline: none;
font-family: 'Open Sans', Helvetica, sans-serif;
font-size: 1rem;
font-weight: bold;
box-shadow: none;
#border-radius: 0;
color: black;
@ -148,6 +155,7 @@
}
.item > label {
font-weight:normal;
display: inline;
text-transform: none;
}
@ -161,4 +169,59 @@
margin:30px 0 20px;
}
.current-subscription {
h2 {
font-weight: bold;
font-size: 22px;
margin-bottom:15px;
color:white;
width:35rem;
}
p.explainer{
width:35rem;
}
label {
text-transform: none;
font-size:1rem;
color:white;
}
button,a {
margin:0;
}
.plan-name {
font-weight:bold;
}
.subscription-plan-and-status {
margin-top:30px;
}
.subscription-actions {
margin-top:10px;
}
.subscription-plan {
display:inline-block;
width:50%;
padding-right:20px;
}
.subscription-status {
display:inline-block;
width:50%;
}
p {
margin:0;
}
.update-plan {
float:right;
}
.effective-subscription {
margin-top:30px;
margin-bottom:60px;
width:35rem;
}
.explanation {
margin-bottom:20px;
}
.update-payment-method {
float:right;
}
}
}

View File

@ -342,11 +342,24 @@ body.web {
}
.create-account-header {
.create-account-header, .subscription-type-header {
padding-top:10px;
margin-left:10px;
}
.radio-field {
margin-bottom:5px;
}
.subscription-options {
margin-top:16px;
margin-left:49px;
label {
display:inline;
}
}
.actions {
margin-top:20px;
input, a {
@ -401,8 +414,63 @@ body.web {
}
.left-side {
float:left;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
width:30rem;
}
.right-side {
margin-left:25px;
margin-left:40px;
float:left;
width:25rem;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
}
@media (min-width:320px){
.left-side {
float:none;
clear:both;
}
.right-side {
margin-top:20px;
margin-left:0;
float:none;
clear:both;
}
}
@media (min-width:900px){
.left-side {
float:left;
clear:none;
}
.right-side {
margin-top:0;
margin-left:40px;
float:left;
width:25rem;
clear:none;
}
}
.comparison-block {
margin:15px 0 40px 25px;
a {
margin-bottom:15px;
}
}
.pricing-info {
font-style:italic;
}

View File

@ -90,9 +90,20 @@ class ApiRecurlyController < ApiController
# get Recurly payment history
def payment_history
@payments=@client.payment_history(current_user)
@payments=@client.payment_history(current_user, params)
render :json => {payments: @payments}
rescue RecurlyClientError => x
puts "Recurly error #{current_user.email}, #{x}"
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
def invoice_history
@invoices, account= @client.invoice_history(current_user, params)
past_due = account && account.state == 'past_due'
render :json => {entries: @invoices, past_due: past_due}
rescue RecurlyClientError => x
puts "Recurly error #{current_user.email}, #{x}"
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
@ -124,6 +135,30 @@ class ApiRecurlyController < ApiController
render json: {message: x.inspect, errors: x.errors}, :status => 404
end
def update_payment
begin
recurly_token = params[:recurly_token]
account = @client.find_or_create_account(current_user, nil, recurly_token)
@client.update_billing_info_from_token(current_user, account, recurly_token)
current_user.update_attribute(:stored_credit_card, true)
past_due = account && account.has_past_due_invoice
subscription = nil
plan_code = current_user.desired_plan_code
result, subscription, account = @client.handle_create_subscription(current_user, plan_code, account)
has_billing_info = !account.nil? && !account[:billing_info].nil?
render :json => {
past_due: past_due,
subscription: subscription, has_billing_info: has_billing_info,
plan_code: current_user.subscription_plan_code,
desired_plan_code: current_user.desired_plan_code,
in_trial: !current_user.subscription_trial_ended?,
trial_ends_at: current_user.subscription_trial_ends_at
}, :status => 200
rescue RecurlyClientError => x
render json: {:message => x.inspect, errors: x.errors}, :status => 404
end
end
def create_subscription
begin
sale = Sale.purchase_subscription(current_user, params[:recurly_token], params[:plan_code])
@ -135,15 +170,21 @@ class ApiRecurlyController < ApiController
end
def get_subscription
subscription = @client.find_subscription(current_user)
subscription, account = @client.find_subscription(current_user)
past_due = account && account.has_past_due_invoice
has_billing_info = !account.nil? && !account[:billing_info].nil?
if subscription
render :json => subscription.to_json
else
render :json => {}
end
render :json => {
past_due: past_due,
subscription: subscription, has_billing_info: has_billing_info,
plan_code: current_user.subscription_plan_code,
desired_plan_code: current_user.desired_plan_code,
in_trial: !current_user.subscription_trial_ended?,
trial_ends_at: current_user.subscription_trial_ends_at
}, :status => 200
end
def cancel_subscription
begin
@client.cancel_subscription(current_user.recurly_subscription_id)
@ -161,19 +202,45 @@ class ApiRecurlyController < ApiController
def change_subscription_plan
begin
result = @client.change_subscription_plan(current_user, params[:plan_code])
plan_code = params[:plan_code]
if plan_code == ''
plan_code = nil
end
result, subscription, account = @client.update_desired_subscription(current_user, plan_code)
has_billing_info = !account.nil? && !account[:billing_info].nil?
past_due = account && account.has_past_due_invoice
if !result
render json: {:message => "No change made to plan"}, :status => 422
else
subscription = Recurly::Subscription.find(current_user.recurly_subscription_id)
render :json => subscription.to_json
render :json => {
past_due: past_due,
subscription:subscription,
has_billing_info: has_billing_info,
plan_code: current_user.subscription_plan_code,
desired_plan_code: current_user.desired_plan_code,
in_trial: !current_user.subscription_trial_ended?,
trial_ends_at: current_user.subscription_trial_ends_at
}
end
rescue RecurlyClientError => x
render json: {:message => x.inspect, errors: x.errors}, :status => 404
end
end
def list_invoices
begin
account = @client.get_account(current_user)
if account.nil?
render json:[], :status => 200
else
return @client.list_invoices(current_user)
end
end
end
def change_subscription_payment
begin
@client.change_subscription_payment(current_user.recurly_subscription_id, params[:recurly_token], params[:billing_info])

View File

@ -42,6 +42,8 @@ class UsersController < ApplicationController
@invited_user = load_invited_user(params)
@plan_code = params[:plan_code] || ''
if @invited_user && @invited_user.email
user = User.find_by_email(@invited_user.email)
if user && (!current_user || current_user.id != user.id)
@ -162,6 +164,11 @@ class UsersController < ApplicationController
birth_date = fixup_birthday(params[:jam_ruby_user]["birth_date(2i)"], params[:jam_ruby_user]["birth_date(3i)"], params[:jam_ruby_user]["birth_date(1i)"])
location = { :country => params[:jam_ruby_user][:country], :state => params[:jam_ruby_user][:state], :city => params[:jam_ruby_user][:city]}
desired_plan_code = params[:jam_ruby_user][:desired_plan_code]
if desired_plan_code ==''
desired_plan_code = nil
end
terms_of_service = params[:jam_ruby_user][:terms_of_service].nil? || params[:jam_ruby_user][:terms_of_service] == "0"? false : true
musician = params[:jam_ruby_user][:musician]
@ -183,7 +190,8 @@ class UsersController < ApplicationController
affiliate_referral_id: cookies[:affiliate_visitor],
affiliate_partner: @affiliate_partner,
timezone: current_timezone,
origin: origin_cookie)
origin: origin_cookie,
desired_plan_code: desired_plan_code )
# check for errors
if @user.errors.any?
@ -191,6 +199,7 @@ class UsersController < ApplicationController
load_location(request.remote_ip, location)
gon.signup_errors = true
gon.musician_instruments = instruments
gon.plan_code = desired_plan_code
render "new", :layout => 'web'
else
sign_in @user

View File

@ -16,7 +16,7 @@ else
attributes :id, :name, :description, :musician_access, :approval_required, :friends_can_join, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id, :track_changes_counter, :max_score, :backing_track_path, :metronome_active, :jam_track_initiator_id, :jam_track_id, :music_session_id_int
if @on_join
node :subscription_rules do |session|
node :subscription do |session|
@subscription_rules
end

View File

@ -29,7 +29,7 @@
<h2>subscription: </h2>
</div>
<div class="account-mid subscription">
Manage your JamKazam subscription
Manage your JamKazam subscription.
</div>
<div class="right">
<a id="account-manage-subscription" href="#" class="button-orange">UPDATE</a>
@ -38,15 +38,25 @@
<hr/>
<div class="account-left">
<h2>sessions: </h2>
<h2>payments:</h2>
</div>
<div class="account-mid sessions">
{{data.session}}
<div class="account-mid payments">
<div class="whitespace">
{% if (data.sales_count == 0) { %}
Enter payment method and view payment history.
{% } else { %}
You have made {{data.sales_count}} purchase{{data.sales_count == 1 ? '' : 's'}}.
{% } %}
</div>
</div>
<div class="right">
<a id="account-scheduled-sessions-link" href="#" class="button-orange">VIEW</a>
<a id="account-payment-history-link" href="#" class="button-orange">VIEW</a>
</div>
<br clear="all" />
<hr/>
{% if (data.isNativeClient) { %}
@ -140,30 +150,6 @@
</div>
<br clear="all" />
{% if (data.musician) { %}
<hr />
<div class="account-left">
<h2>payments:</h2>
</div>
<div class="account-mid payments">
<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">
<a id="account-payment-history-link" href="#" class="button-orange">VIEW</a>
</div>
<br clear="all" />
{% } %}
<hr />
<div class="account-left">

View File

@ -3,7 +3,7 @@
.content-icon
= image_tag "content/icon_account.png", :size => "27x20"
h1
| subscription
| my account
= render "screen_navigation"
.content-body
= react_component 'AccountSubscriptionScreen', {}

View File

@ -280,6 +280,15 @@ script type="text/template" id="template-no-remaining-session-play-time"
.no-remaining-session-play-time
p There is no more time left in this session. You can upgrade your plan.
script type="text/template" id="template-plan-no-record"
.no-remaining-session-play-time
p Recording is not possible on your current plan. You can upgrade your plan to create recordings.
script type="text/template" id="template-plan-no-video"
.no-remaining-session-play-time
p Video is not possible on your current plan. You can upgrade your plan to use video.
script type="text/template" id="template-no-remaining-month-play-time"
.no-remaining-monthly-play-time
p There is no more session time left for this month. You can upgrade your plan.

View File

@ -6,7 +6,7 @@
.spinner-large
h2.create-account-header
.badge-number 2
.badge-number 3
| Download the free JamKazam app
.download-content
@ -26,7 +26,7 @@
.jamtracks
h2.shop-jamtracks
.badge-number 3
.badge-number 4
| Get your free JamTrack
span.special-value
| &nbsp; ($2.99 value)

View File

@ -3,11 +3,15 @@
<% provide(:description, 'Sign up for your JamKazam account to play music with others online in real time and network with musicians.') %>
<div class="content-wrapper register-page">
<h2 class="create-account-header"><div class="badge-number">1</div>Create your free JamKazam account</h2>
<%= form_for(@user, :url => @signup_postback, :method => :post) do |f| %>
<div class="register-container">
<div class="left-side">
<h2 class="create-account-header"><div class="badge-number">1</div>Create your free JamKazam account</h2>
<div class="register-container">
<div class="register-as">
Register as a:
<%= f.radio_button :musician, true, :class => "register-as register-musician" %>
@ -22,7 +26,7 @@
<%= f.text_field :first_name %>
</div>
<div class="field last_name ]>
<div class="field last_name">
<%= f.label :last_name, "Last Name:" %>
<%= f.text_field :last_name %>
</div>
@ -60,19 +64,48 @@
<% end %>
</div>
<div class="actions">
<%= f.submit "CREATE ACCOUNT", class: "button-orange disabled", id:"create-account-submit"%>
<%= link_to "CANCEL", root_path, :class=>'button-grey' %>
</div>
<br clear="all"/>
</div>
</div>
<div class="right-side">
<h2 class="subscription-type-header"><div class="badge-number">2</div>Select your subscription plan</h2>
<div class="subscription-options">
<div class="radio-field">
<%= f.radio_button :desired_plan_code, '', :checked => true %>
<label for="male">Free Plan ($0.00/month)</label>
</div>
<div class="radio-field">
<%= f.radio_button :desired_plan_code, 'jamsubsilver'%>
<label for="male">Silver Plan ($4.99/month)</label>
</div>
<div class="radio-field">
<%= f.radio_button :desired_plan_code, 'jamsubgold' %>
<label for="male">Gold Plan ($9.99/month)</label>
</div>
<div class="radio-field">
<%= f.radio_button :desired_plan_code, 'jamsubplatinum'%>
<label for="male">Platinum Plan ($19.99/month)</label>
</div>
<div class="comparison-block">
<a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-" target="_blank">compare plans</a>
<div class="pricing-info">New users have access to the Gold plan free for the first 30 days. The plan you select will take effect after the first 30-day trial period. Taxes apply to all non-free plans.</div>
</div>
<div class="actions">
<%= f.submit "CREATE ACCOUNT", class: "button-orange disabled", id:"create-account-submit"%>
<%= link_to "CANCEL", root_path, :class=>'button-grey' %>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
<!-- end inner -->
<!-- end overlay content -->
@ -109,6 +142,15 @@
$('#' + value.instrument_id.replace(" ", "") + "_proficiency").val(value.proficiency_level.toString())
})
}
if(window.gon) {
var $radios = $('input:radio[name="jam_ruby_user[desired_plan_code]"]');
if(gon.plan_code) {
$radios.filter(gon.plan_code, true)
}
else {
$radios.filter('[value="<%= @plan_code %>"]').prop('checked', true)
}
}
// show signup errors, if any
if (window.gon && gon.signup_errors) {

View File

@ -482,6 +482,13 @@ if defined?(Bundler)
config.root_redirect_on = true
config.root_redirect_subdomain = ''
config.root_redirect_path = '/'
config.subscription_codes = [{id: 'jamsubsilver', name: 'Silver', price: 4.99}, {id: 'jamsubgold', name: 'Gold', price:9.99}, {id: 'jamsubplatinum', name: 'Platinum', price:19.99}]
config.subscription_codes = [
{id: nil, name: 'Free', price: 0.00, cycle: 'month'},
{id: 'jamsubsilver', name: 'Silver', price: 4.99, cycle: 'month'},
{id: 'jamsubgold', name: 'Gold', price:9.99, cycle: 'month'},
{id: 'jamsubplatinum', name: 'Platinum', price:19.99, cycle: 'month'},
{id: 'jamsubsilveryearly', name: 'Silver', price: 49.99, cycle: 'year'},
{id: 'jamsubgoldyearly', name: 'Gold', price:99.99, cycle: 'year'},
{id: 'jamsubplatinumyearly', name: 'Platinum', price:199.99, cycle: 'year'}]
end
end

View File

@ -394,8 +394,10 @@ Rails.application.routes.draw do
match '/recurly/delete_account' => 'api_recurly#delete_account', :via => :delete
match '/recurly/get_account' => 'api_recurly#get_account', :via => :get
match '/recurly/payment_history' => 'api_recurly#payment_history', :via => :get
match '/recurly/invoice_history' => 'api_recurly#invoice_history', :via => :get
#match '/recurly/get_subscription' => 'api_recurly#get_subscription', :via => :get
match '/recurly/update_account' => 'api_recurly#update_account', :via => :put
match '/recurly/update_payment' => 'api_recurly#update_payment', :via => :post
match '/recurly/billing_info' => 'api_recurly#billing_info', :via => :get
match '/recurly/update_billing_info' => 'api_recurly#update_billing_info', :via => :put
match '/recurly/place_order' => 'api_recurly#place_order', :via => :post

View File

@ -51,6 +51,7 @@ class UserManager < BaseManager
license_start = options[:license_start]
license_end = options[:license_end]
import_source = options[:import_source]
desired_plan_code = options[:desired_plan_code]
recaptcha_failed = false
unless options[:skip_recaptcha] # allow callers to opt-of recaptcha
@ -112,7 +113,8 @@ class UserManager < BaseManager
platform_instructor: platform_instructor,
license_start: license_start,
license_end: license_end,
import_source: import_source)
import_source: import_source,
desired_plan_code: desired_plan_code)
user
end

View File

@ -754,6 +754,7 @@ module JamWebsockets
client_id = options["client_id"]
reconnect_music_session_id = options["music_session_id"]
client_type = options["client_type"]
machine_fingerprint = options["machine"]
os = options["os"]
udp_reachable = options["udp_reachable"].nil? ? true : options["udp_reachable"] == 'true'
jamblaster_serial_no = options["jamblaster_serial_no"]
@ -866,7 +867,12 @@ module JamWebsockets
heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(user, client_type)
@log.debug "logged in #{user} with client_id: #{client_id}"
@log.debug "logged in #{user} with client_id: #{client_id} and fingerprint #{machine_fingerprint}"
# track fingerprint
if machine_fingerprint && user.client_fingerprint != machine_fingerprint
user.update_attribute(:client_fingerprint, machine_fingerprint)
end
# check if there's a connection for the client... if it's stale, reconnect it
if !connection.nil? && connecting
@ -973,7 +979,8 @@ module JamWebsockets
user.name,
connection.client_id_int,
client_update,
arses)
arses,
user.subscription_rules(false))
stats_logged_in
send_to_client(client, login_ack)