From cf00671123f029671b94afeb2307e71c58637fdd Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sun, 29 Nov 2020 18:24:28 -0600 Subject: [PATCH] code-complete minus a few features --- admin/Gemfile | 2 +- admin/Gemfile.lock | 4 +- admin/app/admin/jam_ruby_users.rb | 150 ++++++- admin/config/application.rb | 9 +- admin/config/environments/development.rb | 1 + admin/config/initializers/recurly.rb | 9 + db/up/find_sessions_2020.sql | 51 ++- db/up/scores_better_test_data.sql | 6 +- .../scores_create_schemas_and_extensions.sql | 4 +- db/up/scores_create_tables.sql | 18 +- pb/src/client_container.proto | 15 + ruby/lib/jam_ruby/message_factory.rb | 17 +- ruby/lib/jam_ruby/models/sale.rb | 8 +- ruby/lib/jam_ruby/models/user.rb | 51 ++- ruby/lib/jam_ruby/recurly_client.rb | 363 ++++++++++++++-- ruby/lib/jam_ruby/subscription_definitions.rb | 60 ++- .../models/user_subscriptions_spec.rb | 46 ++ web/app/assets/javascripts/JamServer.js | 21 +- web/app/assets/javascripts/fakeJamClient.js | 9 + web/app/assets/javascripts/jam_rest.js | 26 +- web/app/assets/javascripts/landing/signup.js | 2 + .../AccountPaymentHistoryScreen.js.jsx.coffee | 393 +++++++++++++++--- .../AccountSubscriptionScreen.js.jsx.coffee | 10 +- .../CurrentSubscription.js.jsx.coffee | 186 +++++++-- .../SelectLocation.js.jsx.coffee | 1 - .../SessionRecordBtn.js.jsx.coffee | 24 +- .../SessionVideoBtn.js.jsx.coffee | 25 +- .../Subscription.js.jsx.coffee | 8 +- .../SubscriptionConcern.js.jsx.coffee | 12 +- .../actions/SubscriptionActions.js.coffee | 1 + .../stores/BroadcastStore.js.coffee | 2 +- .../stores/LocationStore.js.coffee | 1 - .../stores/SessionStore.js.coffee | 26 +- .../stores/SubscriptionStore.js.coffee | 28 +- .../stores/WebcamViewer.js.jsx.coffee | 5 + .../client/accountPaymentHistory.scss | 79 +++- .../AccountSubscriptionScreen.scss | 71 +++- web/app/assets/stylesheets/web/main.scss | 72 +++- web/app/controllers/api_recurly_controller.rb | 89 +++- web/app/controllers/users_controller.rb | 11 +- web/app/views/api_music_sessions/show.rabl | 2 +- web/app/views/clients/_account.html.erb | 44 +- .../clients/_account_subscription.html.slim | 2 +- web/app/views/clients/_help.html.slim | 9 + web/app/views/users/_downloads.html.slim | 4 +- web/app/views/users/new.html.erb | 64 ++- web/config/application.rb | 9 +- web/config/routes.rb | 2 + web/lib/user_manager.rb | 4 +- .../lib/jam_websockets/router.rb | 11 +- 50 files changed, 1734 insertions(+), 333 deletions(-) create mode 100644 admin/config/initializers/recurly.rb create mode 100644 ruby/spec/jam_ruby/models/user_subscriptions_spec.rb diff --git a/admin/Gemfile b/admin/Gemfile index bddf8b3c5..cae7f7e72 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -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' diff --git a/admin/Gemfile.lock b/admin/Gemfile.lock index 4d567f4de..3983afb54 100644 --- a/admin/Gemfile.lock +++ b/admin/Gemfile.lock @@ -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) diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index a71f1f13e..d221bc710 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -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 diff --git a/admin/config/application.rb b/admin/config/application.rb index cffb75506..17dfaaf20 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -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" diff --git a/admin/config/environments/development.rb b/admin/config/environments/development.rb index d64ae9a95..f9895b612 100644 --- a/admin/config/environments/development.rb +++ b/admin/config/environments/development.rb @@ -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' diff --git a/admin/config/initializers/recurly.rb b/admin/config/initializers/recurly.rb new file mode 100644 index 000000000..dd309256e --- /dev/null +++ b/admin/config/initializers/recurly.rb @@ -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, + #... +} diff --git a/db/up/find_sessions_2020.sql b/db/up/find_sessions_2020.sql index b8083a598..9b11418c4 100644 --- a/db/up/find_sessions_2020.sql +++ b/db/up/find_sessions_2020.sql @@ -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); diff --git a/db/up/scores_better_test_data.sql b/db/up/scores_better_test_data.sql index 0f5935cc6..308a1d0f2 100644 --- a/db/up/scores_better_test_data.sql +++ b/db/up/scores_better_test_data.sql @@ -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( diff --git a/db/up/scores_create_schemas_and_extensions.sql b/db/up/scores_create_schemas_and_extensions.sql index a4261ccbd..226511ce4 100644 --- a/db/up/scores_create_schemas_and_extensions.sql +++ b/db/up/scores_create_schemas_and_extensions.sql @@ -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; diff --git a/db/up/scores_create_tables.sql b/db/up/scores_create_tables.sql index 4dfad4cc6..2c7f516d5 100644 --- a/db/up/scores_create_tables.sql +++ b/db/up/scores_create_tables.sql @@ -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); diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 74b1ea13d..7df51394f 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -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; diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index f4d9844ec..ef8446ca8 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -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( diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 2674a6d18..73771a19e 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 6608ab11e..0d38beaab 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -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 diff --git a/ruby/lib/jam_ruby/recurly_client.rb b/ruby/lib/jam_ruby/recurly_client.rb index d4d56f03e..de88cf060 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -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) diff --git a/ruby/lib/jam_ruby/subscription_definitions.rb b/ruby/lib/jam_ruby/subscription_definitions.rb index f7e6a4a50..b1410fef5 100644 --- a/ruby/lib/jam_ruby/subscription_definitions.rb +++ b/ruby/lib/jam_ruby/subscription_definitions.rb @@ -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}" diff --git a/ruby/spec/jam_ruby/models/user_subscriptions_spec.rb b/ruby/spec/jam_ruby/models/user_subscriptions_spec.rb new file mode 100644 index 000000000..9efdce26c --- /dev/null +++ b/ruby/spec/jam_ruby/models/user_subscriptions_spec.rb @@ -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 diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index c1ff6e153..52c8b2ee5 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -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); diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index cfc1507aa..c515d9d46 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -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"; }; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index de1fbbf72..cb17f10da 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -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); diff --git a/web/app/assets/javascripts/landing/signup.js b/web/app/assets/javascripts/landing/signup.js index 0eec67cf4..780e4c64a 100644 --- a/web/app/assets/javascripts/landing/signup.js +++ b/web/app/assets/javascripts/landing/signup.js @@ -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 diff --git a/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee index 1729087f9..dac2fb1c6 100644 --- a/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AccountPaymentHistoryScreen.js.jsx.coffee @@ -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
` + + 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.

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 = [``] + for countryId, countryInfo of @state.countries + countries.push(``) + + country = @state.countries[this.currentCountry()] + else + countries = [] + + countryJsx = ` + ` + + if @state.uncollectables.length > 0 uncollectable = @state.uncollectables[0] uncollectableMessage = `
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 support@jamkazam.com so that we can figure out what's gone wrong. Thank you!
` + if @state.pastDue + uncollectableMessage = `
A charge for your subscription has failed. Please update your credit card information immediately so that you can restore your plan.
` 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 = `
BACK {updateCardAction} - SUBMIT CARD INFORMATION + SUBMIT CARD INFORMATION
` else - header = 'You have already entered a credit card in JamKazam.' + header = 'You have have a payment method on file already.' updateCardAction = `I'D LIKE TO UPDATE MY PAYMENT INFO` + managedSubscriptionAction = `MANAGE MY SUBSCRIPTION` actions = `
BACK {updateCardAction} + {managedSubscriptionAction}
` else - header = 'Please enter your billing address and payment information below.' + header = `
Please enter your billing address and payment information below.

This information is sent to Recurly, which is compliant with the PCI-DSS security standard. JamKazam does not receive or store your credit card.
` actions = `
BACKSUBMIT CARD INFORMATION + className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION +
` + + firstNameField = + `
+ + +
` + lastNameField = + `
+ +
` - if @state.shouldShowName && @state.user?.name? - username = @state.user?.name - nameField = - `
- - -
` `
{uncollectableMessage}
{header}
-
- {nameField} -
- - + + {firstNameField} + {lastNameField} +
+ +
-
- - +
+ +
-
- - +
+ + +
+
+ +
- - Postal Code: +
-
- - +
+ + {countryJsx}
- +
+ + +
+ + {actions}
@@ -458,7 +668,6 @@ profileUtils = context.JK.ProfileUtils CHARGED AT - METHOD DESCRIPTION REASON AMOUNT @@ -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 = ` {date} - {paymentMethod} {description} {status} {displayAmount} @@ -509,7 +723,6 @@ profileUtils = context.JK.ProfileUtils DATE - METHOD DESCRIPTION STATUS AMOUNT @@ -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 `
-
payment
history:
+
payment
management:
{profileNav}
@@ -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.

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.

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.'}) diff --git a/web/app/assets/javascripts/react-components/AccountSubscriptionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/AccountSubscriptionScreen.js.jsx.coffee index a988a77b9..ec602c3ed 100644 --- a/web/app/assets/javascripts/react-components/AccountSubscriptionScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AccountSubscriptionScreen.js.jsx.coffee @@ -49,17 +49,12 @@ profileUtils = context.JK.ProfileUtils if @state.subscription - if @state.subscription.plan - currentSubscription = `` + currentSubscription = `` - createSubscription = `` content = `
{currentSubscription}
-
- {createSubscription} -
` else content = `
Loading...
` @@ -72,9 +67,6 @@ profileUtils = context.JK.ProfileUtils
{content} -
- LEAVE -

diff --git a/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee b/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee index 4876b1255..95b9a4b4e 100644 --- a/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee @@ -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(``) + if plan.cycle == 'month' + monthlies.push(plan) + else + yearlies.push(plan) + plan_codes.push(``) + for plan in monthlies + plan_codes.push(``) + plan_codes.push(``) + for plan in yearlies + plan_codes.push(``) + plansJsx = `` - plansJsx = ` - ` - - 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 = `(billed at {this.getDisplayNameTier(this.props.subscription.plan.plan_code)} until next billing cycle` + recurly_subscription = @props.subscription.subscription + effective_plan_name = `{this.getDisplayNameTier(this.props.subscription.plan_code)}` + desired_plan_name = `{this.getDisplayNameTier(this.props.subscription.desired_plan_code)}` + admin_override_plan_name = `{this.getDisplayNameTier(this.props.subscription.admin_override_plan_code)}` + 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 = `Billing starts for the {desired_plan_name} plan after the trial ends.` + else + warning = `

You will drop to the Free free plan after the trial ends because you have not yet entered payment info.

` + show_payment_info = true + else + if has_billing_info + warning = `

You will drop to the Free free plan after the trial ends because you have not selected a plan.

` + else + warning = `

You will drop to the Free free plan after the trial ends because you have not yet entered payment info or selected a plan.

` + show_payment_info = true + + explanation = `You have a Gold account until your trial ends {context.JK.formatDateShort(this.props.subscription.trial_ends_at)}. {note}` + else + if @props.subscription.desired_plan_code && !@props.subscription.plan_code && !has_billing_info + explanation = `You have successfully upgraded your plan to the {desired_plan_name} level, thank you!` + warning = `

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.

` + show_payment_info = true + else + if @props.subscription.desired_plan_code + if !has_billing_info + show_payment_info = true + explanation = `You have successfully upgraded your plan to the {desired_plan_name} level, thank you` + warning = `

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.

` + else + explanation = `You have successfully upgraded your plan to the {desired_plan_name} level, thank you!` + + else + # free plan situation - not much to go on about + explanation = `You are currently on the {desired_plan_name} plan.` + if show_payment_info + update_payment_btn = `UPDATE PAYMENT METHOD` + if has_pending_subscription + billingAddendum = null #`

You will be billed next at the {this.getDisplayNameTier(this.props.subscription.subscription.plan.plan_code)} on the next billing cycle.
` + else if cancelled_subscription && this.props.subscription.desired_plan_code == null && this.props.subscription.plan_code != null + billingAddendum = `However, your cancelled {effective_plan_name} plan is still active until the end of the billing cycle.`# `

You will be billed a final time at the {this.getDisplayNameTier(this.props.subscription.subscription.plan.plan_code)} at end of this billing cycle.
` else - currentPlan = this.getDisplayNameTier(this.props.subscription.plan.plan_code) billingAddendum = null - `
+ `
-

Current Subscription

-
- {currentPlan} - {billingAddendum} +

Subscription:

+

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.

+
+
+
+ +
+ {plansJsx} + COMPARE PLANS + +
+
+
+
+
+
+

{explanation} {billingAddendum}

+ {warning} +
+ BACK{update_payment_btn}
-
-

Change Plan

-
- - - {plansJsx} - - -
-
-
-

Cancel Plan

- -
` }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee index ccb3522a9..398e64106 100644 --- a/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SelectLocation.js.jsx.coffee @@ -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) -> diff --git a/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee index c6d75af93..935da51f9 100644 --- a/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee @@ -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: () -> ` diff --git a/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee index d89f7ae29..604f77502 100644 --- a/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee @@ -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: () -> ` diff --git a/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee b/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee index 80d32a5bd..7733352ec 100644 --- a/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee @@ -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 - + @@ -145,10 +146,11 @@ UserStore = context.UserStore {countryJsx} + {plansJsx}
-
+
diff --git a/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee b/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee index 71e9fa931..23fdd01e0 100644 --- a/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee @@ -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 = `

You have run out of time.

-

You can upgrade your plan to continue playing.

+

Compare plans and consider upgrading your plan for more play time and premium features.

` else if @props.subscription.main_concern_type == 'remaining_session_play_time' content = `

You will run out play time for this session in:

{this.displayTime()}

-

You can upgrade your plan to continue playing.

+

Compare plans and consider upgrading your plan for more play time and premium features.

` else content = `

You will run out of monthly play time in:

{this.displayTime()}

-

You can upgrade your plan to continue playing.

+

Compare plans and consider upgrading your plan for more play time and premium features.

` if content? diff --git a/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee index b06291f16..203159b94 100644 --- a/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee @@ -5,4 +5,5 @@ context = window changeSubscription: {} cancelSubscription: {} updatePayment: {} + forceUpdate: {} }) diff --git a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee index 6c98178c2..a13e95b72 100644 --- a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee @@ -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 = [] diff --git a/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee b/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee index 00a07bbfc..5c68e9ef5 100644 --- a/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/LocationStore.js.coffee @@ -13,7 +13,6 @@ logger = context.JK.logger this.listenTo(context.AppStore, this.onAppInit) changed: () -> - console.log("changed", @countries) @trigger(@countries) onSelectCountry: (country) -> diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index c5c7aa4b9..64f1ab68a 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -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 diff --git a/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee index a36a92a84..ddcc44b43 100644 --- a/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee @@ -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) => diff --git a/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee index 9c87f9e9f..39cd7dea4 100644 --- a/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee @@ -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 diff --git a/web/app/assets/stylesheets/client/accountPaymentHistory.scss b/web/app/assets/stylesheets/client/accountPaymentHistory.scss index 011aa00a8..3bd0487a3 100644 --- a/web/app/assets/stylesheets/client/accountPaymentHistory.scss +++ b/web/app/assets/stylesheets/client/accountPaymentHistory.scss @@ -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; diff --git a/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss b/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss index 02b05d07a..21267f859 100644 --- a/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss +++ b/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss @@ -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; + } + } } diff --git a/web/app/assets/stylesheets/web/main.scss b/web/app/assets/stylesheets/web/main.scss index 7cc20672b..d1d79a0ac 100644 --- a/web/app/assets/stylesheets/web/main.scss +++ b/web/app/assets/stylesheets/web/main.scss @@ -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; } diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index 1062e4fba..c24be2e21 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -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]) diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index d254726f8..0540a8c88 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -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 diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index b0c5a2544..1dc965c2e 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -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 diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index f94a62980..c3b30ba40 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -29,7 +29,7 @@

subscription:

UPDATE @@ -38,15 +38,25 @@
-
- {% if (data.musician) { %} -
- - - - - -
- VIEW -
-
- - {% } %} -
@@ -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) { diff --git a/web/config/application.rb b/web/config/application.rb index d17c1ef02..aeac7ff2e 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -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 diff --git a/web/config/routes.rb b/web/config/routes.rb index da504ac93..63f2c52fc 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -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 diff --git a/web/lib/user_manager.rb b/web/lib/user_manager.rb index 233b2c007..91c85ac67 100644 --- a/web/lib/user_manager.rb +++ b/web/lib/user_manager.rb @@ -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 diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 31cbe51ac..553e0a79b 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -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)