From ce301fd1450795db44334a9eb0d924205f7414d6 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 21 Nov 2020 16:14:37 -0600 Subject: [PATCH] session limits done --- db/up/find_sessions_2020.sql | 10 ++ pb/src/client_container.proto | 17 +++ ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/connection_manager.rb | 20 ++- .../jam_ruby/constants/validation_messages.rb | 1 + .../jam_ruby/models/active_music_session.rb | 10 ++ ruby/lib/jam_ruby/models/connection.rb | 12 ++ ruby/lib/jam_ruby/models/sale.rb | 6 +- ruby/lib/jam_ruby/models/sale_line_item.rb | 2 +- ruby/lib/jam_ruby/models/user.rb | 15 ++ ruby/lib/jam_ruby/recurly_client.rb | 102 +++++++++++-- ruby/lib/jam_ruby/subscription_definitions.rb | 76 ++++++++++ web/Gemfile | 4 +- web/Gemfile.lock | 8 +- web/app/assets/javascripts/dialog/banner.js | 10 +- web/app/assets/javascripts/fakeJamClient.js | 18 ++- web/app/assets/javascripts/jam_rest.js | 46 ++++++ .../assets/javascripts/notificationPanel.js | 28 ++++ .../AccountSubscriptionScreen.js.jsx.coffee | 39 +++-- .../CurrentSubscription.js.jsx.coffee | 99 ++++++++++++ .../Subscription.js.jsx.coffee | 40 +++-- .../SubscriptionConcern.js.jsx.coffee | 83 +++++++++++ .../TopMessageHolder.js.jsx.coffee | 12 +- .../actions/SubscriptionActions.js.coffee | 8 + .../helpers/SessionHelper.js.coffee | 4 +- .../stores/BroadcastStore.js.coffee | 141 +++++++++++++----- .../stores/SessionStore.js.coffee | 73 ++++++++- .../stores/SubscriptionStore.js.coffee | 66 ++++++++ web/app/assets/javascripts/utils.js | 19 ++- .../AccountSubscriptionScreen.scss | 29 +++- .../client/react-components/broadcast.scss | 13 ++ .../api_music_sessions_controller.rb | 19 ++- web/app/controllers/api_recurly_controller.rb | 29 ++-- web/app/controllers/api_users_controller.rb | 4 +- web/app/controllers/sessions_controller.rb | 11 ++ web/app/helpers/music_session_helper.rb | 2 + web/app/views/api_music_sessions/show.rabl | 12 +- .../clients/_account_subscription.html.slim | 2 +- web/app/views/clients/_help.html.slim | 12 ++ web/config/application.rb | 2 +- web/config/initializers/gon.rb | 1 + web/config/routes.rb | 6 + .../lib/jam_websockets/router.rb | 2 +- 43 files changed, 1002 insertions(+), 112 deletions(-) create mode 100644 ruby/lib/jam_ruby/subscription_definitions.rb create mode 100644 web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee diff --git a/db/up/find_sessions_2020.sql b/db/up/find_sessions_2020.sql index 895f03624..b8083a598 100644 --- a/db/up/find_sessions_2020.sql +++ b/db/up/find_sessions_2020.sql @@ -96,6 +96,7 @@ ALTER TABLE arses ADD COLUMN continent VARCHAR(200); 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, @@ -113,3 +114,12 @@ CREATE TABLE subscriptions ( created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); + +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); + +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); +CREATE INDEX msuh_created_at ON music_sessions_user_history USING btree (created_at); \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 0eb19091a..74b1ea13d 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -54,6 +54,10 @@ message ClientMessage { SCHEDULED_SESSION_RESCHEDULED = 180; SCHEDULED_SESSION_REMINDER = 181; SCHEDULED_SESSION_COMMENT = 182; + SESSION_KICK = 183; + + // subscription-related + SUBSCRIPTION_CHANGED = 195; // recording notifications MUSICIAN_RECORDING_SAVED = 200; @@ -177,6 +181,10 @@ message ClientMessage { optional ScheduledSessionRescheduled scheduled_session_rescheduled = 180; optional ScheduledSessionReminder scheduled_session_reminder = 181; optional ScheduledSessionComment scheduled_session_comment = 182; + optional SessionKick session_kick = 183; + + // subscription-related + optional SubscriptionChanged subscription_changed = 195; // recording notifications optional MusicianRecordingSaved musician_recording_saved = 200; @@ -528,6 +536,15 @@ message ScheduledSessionComment { optional string created_at = 8; } +message SessionKick { + optional string session_id = 1; + optional string reason = 2; +} + +message SubscriptionChanged { + +} + message MusicianRecordingSaved { optional string recording_id = 1; optional string photo_url = 2; diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index fae069679..02dd9f955 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -54,6 +54,7 @@ require "jam_ruby/lib/em_helper" require "jam_ruby/lib/nav" require "jam_ruby/lib/html_sanitize" require "jam_ruby/lib/guitar_center" +require "jam_ruby/subscription_definitions" require "jam_ruby/resque/resque_jam_error" require "jam_ruby/resque/resque_hooks" require "jam_ruby/resque/audiomixer" diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 384110fa6..4d79477be 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -352,7 +352,7 @@ SQL end end - def update_session_controller(music_session_id) + def update_session_controller(music_session_id, kick_extras = false) tracks_changed = false active_music_session = ActiveMusicSession.find(music_session_id) @@ -362,7 +362,22 @@ SQL # find next in line, because the current 'session controller' is not part of the session tracks_changed = next_in_line(music_session, active_music_session) end + + + if kick_extras + num_participants = active_music_session.users.count + + puts("kick extras = num_participants #{num_participants}") + active_music_session.users.each do |user| + subscription_rules = user.subscription_rules(false) + puts "checking max players for #{user.email} #{subscription_rules[:max_players]}" + if subscription_rules[:max_players] && subscription_rules[:max_players] < num_participants + puts "kicking user #{user.email}" + end + end + end end + tracks_changed end @@ -436,7 +451,8 @@ SQL if connection.errors.any? raise ActiveRecord::Rollback else - tracks_changed = update_session_controller(music_session.id) + tracks_changed = update_session_controller(music_session.id, kick_extras = true) + end end diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 7dbaf3bb4..1a0c98f1f 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -66,6 +66,7 @@ module ValidationMessages CAN_ONLY_JOIN_SAME_SCHOOL_SESSION = "You can only join sessions from your school" LICENSE_EXPIRED = "Your license has expired" LICENSE_NOT_STARTED = "Your license has not started" + PLAN_PROHIBIT_MAX_PLAYERS = "The session size is greater than your plan allows" # chat CAN_ONLY_CHAT_SAME_SCHOOL = "You can only message others from your school" diff --git a/ruby/lib/jam_ruby/models/active_music_session.rb b/ruby/lib/jam_ruby/models/active_music_session.rb index 489cc3e8a..a85745632 100644 --- a/ruby/lib/jam_ruby/models/active_music_session.rb +++ b/ruby/lib/jam_ruby/models/active_music_session.rb @@ -936,6 +936,16 @@ module JamRuby self.save end + def play_time_remaining(user) + rules = SubscriptionDefinitions.rules(user.subscription_plan_code) + play_time_per_session = rules[:play_time_per_session] + if play_time_per_session.nil? + nil + else + (play_time_per_session * 3600) - MusicSessionUserHistory.where(music_session_id: self.id).where(user_id: user.id).sum("extract('epoch' from (COALESCE(session_removed_at, NOW()) - created_at))") + end + end + def self.sync(session_history) music_session = MusicSession.find_by_id(session_history.id) diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 3481e99b1..73b2ad774 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -166,6 +166,18 @@ module JamRuby errors.add(:music_session, ValidationMessages::LICENSE_NOT_STARTED) end + num_participants = music_session.users.count + + puts "NUM PARTICIPANTS BEFORE JOIN #{num_participants}" + subscription_rules = self.user.subscription_rules(dynamic_definitions = false) + + max_players = subscription_rules[:max_players] + if !max_players.nil? + if num_participants >= max_players + errors.add(:music_session, ValidationMessages::PLAN_PROHIBIT_MAX_PLAYERS) + end + end + # unless user.admin? # num_sessions = Connection.where(:user_id => user_id) # .where(["(music_session_id IS NOT NULL) AND (aasm_state != ?)",EXPIRED_STATE.to_s]) diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index 3ef61a370..2674a6d18 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -274,7 +274,10 @@ module JamRuby if sale.valid? client = RecurlyClient.new - account = client.get_account(current_user) + + account = client.find_or_create_account(current_user, nil, recurly_token) + + client.update_billing_info_from_token(current_user, account, recurly_token) if account.present? recurly_response = client.create_subscription(current_user, plan_code, account) @@ -288,6 +291,7 @@ module JamRuby sale.recurly_currency = recurly_response.currency sale.save(validate: false) else + puts "Could not find account to place order." raise RecurlyClientError, "Could not find account to place order." end end diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 2e6c7d203..5eea7fe1f 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -48,7 +48,7 @@ module JamRuby def product if product_type == JAMTRACK JamTrack.find_by_id(product_id) - if product_type == SUBSCRIPTION + elsif product_type == SUBSCRIPTION {name: product_id} elsif product_type == GIFTCARD GiftCardType.find_by_id(product_id) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index f86bf186b..6608ab11e 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -2814,6 +2814,21 @@ module JamRuby end end + + def subscription_rules(dynamic_definitions = true) + rules = SubscriptionDefinitions.rules(self.subscription_plan_code) + if dynamic_definitions + play_time_per_month = rules[:play_time_per_month] + if play_time_per_month.nil? + rules[:remaining_month_play_time] = nil + else + rules[:remaining_month_play_time] = (play_time_per_month * 3600) - MusicSessionUserHistory.where(user_id: self.id).where("date_trunc('month', created_at) = date_trunc('month', NOW())").where('session_removed_at IS NOT NULL').sum("extract('epoch' from (session_removed_at - created_at))") + end + end + + rules + 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 5530fe1a8..d4d56f03e 100644 --- a/ruby/lib/jam_ruby/recurly_client.rb +++ b/ruby/lib/jam_ruby/recurly_client.rb @@ -11,9 +11,11 @@ module JamRuby begin #puts "Recurly.api_key: #{Recurly.api_key}" account = Recurly::Account.create(options) - raise RecurlyClientError.new(account.errors) if account.errors.any? + if account.errors.any? + puts "Errors encountered while creating account: #{account.errors}" + raise RecurlyClientError.new(account.errors) if account.errors.any? + end rescue Recurly::Error, NoMethodError => x - #puts "Error: #{x} : #{Kernel.caller}" raise RecurlyClientError, x.to_s else if account @@ -30,7 +32,7 @@ module JamRuby def delete_account(current_user) account = get_account(current_user) - if (account) + if account begin account.destroy rescue Recurly::Error, NoMethodError => x @@ -43,11 +45,26 @@ module JamRuby end def get_account(current_user) - current_user && current_user.recurly_code ? Recurly::Account.find(current_user.recurly_code) : nil + 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) + + if !account + account = Recurly::Account.find(current_user.id) + # repair user local account info + if !account.nil? + current_user.update_attribute(:recurly_code, account.account_code) + end + end + + account + rescue Recurly::Error => x raise RecurlyClientError, x.to_s end + def update_account(current_user, billing_info=nil) account = get_account(current_user) if(account.present?) @@ -94,9 +111,9 @@ module JamRuby payments end - def update_billing_info(current_user, billing_info=nil) - account = get_account(current_user) - if (account.present?) + def update_billing_info(current_user, billing_info=nil, account = nil) + account = get_account(current_user) if account.nil? + if account.present? begin account.billing_info = billing_info account.billing_info.save @@ -111,6 +128,14 @@ module JamRuby account end + # token was created in the web ui. we can tell recurly to update the billing info on the account with just the token + def update_billing_info_from_token(current_user, account, recurly_token) + account.billing_info = { + token_id: recurly_token + } + account.billing_info.save! + end + def refund_user_subscription(current_user, jam_track) jam_track_right=JamRuby::JamTrackRight.where("user_id=? AND jam_track_id=?", current_user.id, jam_track.id).first if jam_track_right @@ -188,6 +213,7 @@ module JamRuby # 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', @@ -200,14 +226,58 @@ module JamRuby subscription end - def find_subscription(user) + def find_subscription(user, account = nil) + subscription = nil + if user.recurly_subscription_id.nil? - nil + if account.nil? + account = get_account(user) + end + if account + account.subscriptions.find_each do |subscription| + puts "Subscription: #{subscription.inspect}" + end + subscription = account.subscriptions.first + else + puts "can't find subscription for account #{account}" + end else - Recurly::Subscription.find(user.recurly_subscription_id) + subscription = Recurly::Subscription.find(user.recurly_subscription_id) end + + if 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 end + def change_subscription_plan(current_user, plan_code) + subscription = find_subscription(current_user) + + if subscription.nil? + puts "no subscription found for user #{current_user.email}" + return false + end + + puts "subscription.plan #{subscription.plan}" + if subscription.plan.plan_code == plan_code + puts "plan code was the same as requested: #{plan_code}" + return false + end + + result = subscription.update_attributes( + :plan_code => plan_code, + :timeframe => 'bill_date' + ) + puts "change subscription plan #{result}" + + return result + end + + def sync_subscription(user) subscription = find_subscription(user) @@ -225,13 +295,17 @@ module JamRuby end end - def find_or_create_account(current_user, billing_info) + def find_or_create_account(current_user, billing_info, recurly_token = nil) account = get_account(current_user) - if(account.nil?) + if !account account = create_account(current_user, billing_info) - else - update_billing_info(current_user, billing_info) + elsif !billing_info.nil? + update_billing_info(current_user, billing_info, account) + end + + if !recurly_token.nil? + update_billing_info_from_token(current_user, account, recurly_token) end account end diff --git a/ruby/lib/jam_ruby/subscription_definitions.rb b/ruby/lib/jam_ruby/subscription_definitions.rb new file mode 100644 index 000000000..f7e6a4a50 --- /dev/null +++ b/ruby/lib/jam_ruby/subscription_definitions.rb @@ -0,0 +1,76 @@ +module JamRuby + class SubscriptionDefinitions + JAM_SILVER = 'jamsubsilver' + JAM_SILVER_WITH_TRIAL = 'jamsubsilvertrial' + JAM_GOLD = 'jamsubgold' + JAM_GOLD_WITH_TRIAL = 'jamsubgoldtrial' + JAM_PLATINUM = 'jamsubplatinum' + JAM_PLATINUM_WITH_TRIAL = 'jamsubplatinumtrial' + + # ALL IN HOURS + FREE_PLAY_TIME_PER_SESSION = 1 + FREE_PLAY_TIME_PER_MONTH = 4 + SILVER_PLAY_TIME_PER_SESSION = nil # unlimited + SILVER_PLAY_TIME_PER_MONTH = 10 + GOLD_PLAY_TIME_PER_SESSION = nil # unlimited + GOLD_PLAY_TIME_PER_MONTH = nil # unlimited + PLATINUM_PLAY_TIME_PER_SESSION = nil # unlimited + PLATINUM_PLAY_TIME_PER_MONTH = nil # unlimited + + + 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', + max_players: 4 + + } + + 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', + 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', + 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', + 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 + SILVER_PLAN + elsif plan_code == JAM_GOLD || plan_code == JAM_GOLD_WITH_TRIAL + GOLD_PLAN + elsif plan_code == JAM_PLATINUM || plan_code == JAM_PLATINUM_WITH_TRIAL + PLATINUM_PLAN + else + raise "unknown plan #{plan_code}" + end + end + end +end diff --git a/web/Gemfile b/web/Gemfile index b9c63cab7..4238b00b0 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -105,7 +105,7 @@ gem 'rubyzip' gem 'slim' gem 'htmlentities' gem 'sanitize' -gem 'recurly' +gem 'recurly', '~> 2' #gem 'guard', '2.7.3' #gem 'influxdb' #, '0.1.8' gem 'cause' # needed by influxdb @@ -167,7 +167,7 @@ end gem 'sass-rails' gem 'coffee-rails' gem 'uglifier' -gem 'coffee-script-source', '1.11.1' +gem 'coffee-script-source', '1.12.2' group :test, :cucumber do gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' diff --git a/web/Gemfile.lock b/web/Gemfile.lock index defb6ff29..3a3873f27 100644 --- a/web/Gemfile.lock +++ b/web/Gemfile.lock @@ -137,7 +137,7 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.11.1) + coffee-script-source (1.12.2) concurrent-ruby (1.0.5) connection_pool (2.2.1) crass (1.0.2) @@ -577,7 +577,7 @@ GEM execjs rails (>= 3.2) tilt - recurly (2.10.0) + recurly (2.18.16) redis (3.3.3) redis-namespace (1.5.3) redis (~> 3.0, >= 3.0.4) @@ -764,7 +764,7 @@ DEPENDENCIES carrierwave_direct cause coffee-rails - coffee-script-source (= 1.11.1) + coffee-script-source (= 1.12.2) database_cleaner (= 1.3.0) devise (= 3.3.0) em-websocket (>= 0.4.0) @@ -826,7 +826,7 @@ DEPENDENCIES rails-observers railties (> 4.2) react-rails (= 1.3.3) - recurly + recurly (~> 2) responders (~> 2.0) resque resque-dynamic-queues diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index cb6e454bb..5ecbfea9c 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -175,11 +175,17 @@ } else { $btn.click(function() { + var hideOnClick = true if (button.click) { - button.click(); + var result = button.click(); + if(result === 'noclose') { + hideOnClick = false + } } - hide(); + if(hideOnClick) { + hide(); + } return false; }); } diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 7e2da352e..cfc1507aa 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -182,6 +182,14 @@ videoShared = true; } + function IsVstLoaded() { + return false; + } + + function hasVstAssignment() { + return false; + } + function FTUEGetInputLatency() { dbg("FTUEGetInputLatency"); return 2; @@ -793,6 +801,10 @@ logger.debug("Fake JamClient: SessionAudioResync()"); } + function getConnectionDetail(arg1, arg2) { + return {} + } + function SessionGetAllControlState(isMasterOrPersonal) { var mixerIds = SessionGetIDs() return SessionGetControlState(mixerIds, isMasterOrPersonal); @@ -1651,6 +1663,7 @@ this.SessionSetMasterLocalMix = SessionSetMasterLocalMix; this.SessionGetDeviceLatency = SessionGetDeviceLatency; this.SessionAudioResync = SessionAudioResync; + this.getConnectionDetail = getConnectionDetail; // Track this.TrackGetChannels = TrackGetChannels; @@ -1751,7 +1764,10 @@ this.FTUEGetVideoShareEnable = FTUEGetVideoShareEnable; this.isSessVideoShared = isSessVideoShared; this.SessStopVideoSharing = SessStopVideoSharing; - this.SessStartVideoSharing = SessStartVideoSharing; + this.SessStartVideoSharing = SessStartVideoSharing + + this.IsVstLoaded = IsVstLoaded; + this.hasVstAssignment = hasVstAssignment; // Clipboard this.SaveToClipboard = SaveToClipboard; diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 71fd20294..de1fbbf72 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -2802,6 +2802,48 @@ }) } + function createSubscription(options) { + options = options || {} + return $.ajax({ + type: "POST", + url: '/api/recurly/create_subscription', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function getSubscription() { + return $.ajax({ + type: "GET", + url: '/api/recurly/get_subscription', + dataType: "json", + contentType: 'application/json' + }) + } + + function changeSubscription(options) { + options = options || {} + return $.ajax({ + type: "POST", + url: '/api/recurly/change_subscription', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function cancelSubscription(options) { + options = options || {} + return $.ajax({ + type: "POST", + url: '/api/recurly/cancel_subscription', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + function createLiveStream(musicSessionId) { return $.ajax({ type: "POST", @@ -3128,6 +3170,10 @@ this.findFriendSessions = findFriendSessions; this.findPublicSessions = findPublicSessions; this.getConfigClient = getConfigClient; + this.createSubscription = createSubscription; + this.getSubscription = getSubscription; + this.changeSubscription = changeSubscription; + this.cancelSubscription= cancelSubscription; return this; }; })(window, jQuery); diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index 8a1cc8078..121caa43e 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -735,6 +735,34 @@ //decrementNotificationCount(); } + function registerSessionKick() { + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_KICK, function(header, payload) { + logger.debug("Handling SESSION_KICK msg " + JSON.stringify(payload)); + + context.SessionAction.leaveSession({ + location: '/client#/home', + }) + + var buttons = [] + buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'}) + buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: function() { + context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-") + return 'noclose' + }}) + buttons.push({ + name: 'UPGRADE PLAN', + buttonStyle: 'button-orange', + click: function() { + context.JK.popExternalLink("/client#/account/subscription", true) + } + }) + context.JK.Banner.show({ + title: "Session Too Big For Current Plan", + html: context._.template($('#template-session-too-big-kicked').html(), {}, { variable: 'data' }), + buttons: buttons}); + }) + } + function registerSessionInvitation() { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_INVITATION, function(header, payload) { logger.debug("Handling SESSION_INVITATION msg " + JSON.stringify(payload)); 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 1b1c12720..a988a77b9 100644 --- a/web/app/assets/javascripts/react-components/AccountSubscriptionScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/AccountSubscriptionScreen.js.jsx.coffee @@ -4,6 +4,8 @@ logger = context.JK.logger AppStore = context.AppStore UserStore = context.UserStore +SubscriptionStore = context.SubscriptionStore +SubscriptionActions = context.SubscriptionActions profileUtils = context.JK.ProfileUtils @@ -12,7 +14,8 @@ profileUtils = context.JK.ProfileUtils mixins: [ ICheckMixin, Reflux.listenTo(AppStore, "onAppInit"), - Reflux.listenTo(UserStore, "onUserChanged") + Reflux.listenTo(UserStore, "onUserChanged"), + Reflux.listenTo(SubscriptionStore, "onSubscriptionChanged") ] onAppInit: (@app) -> @@ -21,26 +24,22 @@ profileUtils = context.JK.ProfileUtils onUserChanged: (userState) -> @setState({user: userState?.user}) - componentDidUpdate: () -> - console.log("did update") - + onSubscriptionChanged: (subscription) -> + @setState({subscription: subscription}) beforeHide: (e) -> @screenVisible = false return true beforeShow: (e) -> - console.log("before show") + SubscriptionActions.updateSubscription() afterShow: (e) -> @screenVisible = true logger.debug("AccountSubscriptionScreen: afterShow") getInitialState: () -> - { - user: null, - updating: false - } + { user: null, updating: false, subscription: null} onCancel: (e) -> e.preventDefault() @@ -48,15 +47,31 @@ profileUtils = context.JK.ProfileUtils render: () -> + if @state.subscription + + if @state.subscription.plan + currentSubscription = `` + + createSubscription = `` + content = `
+
+ {currentSubscription} +
+
+ {createSubscription} +
+
` + else + content = `
Loading...
` + `
-
subscription:
-
+
- + {content} diff --git a/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee b/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee new file mode 100644 index 000000000..4876b1255 --- /dev/null +++ b/web/app/assets/javascripts/react-components/CurrentSubscription.js.jsx.coffee @@ -0,0 +1,99 @@ +context = window +rest = context.JK.Rest() +logger = context.JK.logger +LocationActions = context.LocationActions +SubscriptionActions = context.SubscriptionActions +UserStore = context.UserStore +AppStore = context.AppStore + +@CurrentSubscription = React.createClass({ + + mixins: [Reflux.listenTo(AppStore, "onAppInit")] + + + getInitialState: () -> + { + selectedPlan: null + } + + getDisplayNameTier: (plan_code) -> + for subscriptionCode in gon.global.subscription_codes + if plan_code == subscriptionCode.id + return subscriptionCode.name + return "Unknown plan code=#{plan_code}" + + getDisplayNamePrice: (plan_code) -> + 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() + @setState({selectedPlan: val}) + + currentPlan: () -> + this.state.selectedPlan || this.props.selectedPlan || '' + + onChangeSubmit: (event) -> + form = event.target + event.preventDefault() + + if !@state.selectedPlan + return + SubscriptionActions.changeSubscription(this.state.selectedPlan) + + onCancelPlan: (event) -> + if confirm("Are you sure you want to cancel your plan?") + SubscriptionActions.cancelSubscription() + + componentDidMount: () -> + + @root = $(@getDOMNode()) + + document.querySelector('#change-subscription-form').addEventListener('submit', @onChangeSubmit.bind(this)) + + render: () -> + plan_codes = [] + for plan in gon.global.subscription_codes + plan_codes.push(``) + + plansJsx = ` + ` + + changeClass = 'button-orange' + if !@state.selectedPlan + 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` + else + currentPlan = this.getDisplayNameTier(this.props.subscription.plan.plan_code) + billingAddendum = null + `
+
+

Current Subscription

+
+ {currentPlan} + {billingAddendum} +
+
+
+

Change Plan

+
+ + + {plansJsx} + + +
+
+
+

Cancel Plan

+ +
+
` + +}) \ No newline at end of file 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 da7f8b60d..80d32a5bd 100644 --- a/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/Subscription.js.jsx.coffee @@ -13,11 +13,13 @@ UserStore = context.UserStore getInitialState: () -> { clicked: false, - selectedCountry: null + selectedCountry: null, + selectedPlan: null, + subscription: null } onLocationsChanged: (countries) -> - console.log("countires in ", countries) + console.log("countries in ", countries) @setState({countries: countries}) onCountryChanged: (e) -> @@ -27,18 +29,26 @@ UserStore = context.UserStore currentCountry: () -> this.state.selectedCountry || this.props.selectedCountry || '' + onPlanChanged: (e) -> + val = $(e.target).val() + @setState({selectedPlan: val}) + + currentPlan: () -> + this.state.selectedPlan || this.props.selectedPlan || '' + openBrowser: () -> context.JK.popExternalLink("https://www.jamkazam.com/client#/subscription") - onRecurlyToken: (err, token) -> - console.log("TOKEN", token) + onRecurlyToken: (err, token_data) -> + if err console.log("error", err) # handle error using err.code and err.fields else # recurly.js has filled in the 'token' field, so now we can submit the # form to your server - console.log("eintercepted") + console.log("eintercepted", token_data) + rest.createSubscription({plan_code: @state.selectedPlan, recurly_token: token_data.id}) onFormSubmit: (event) -> form = event.target @@ -51,6 +61,7 @@ UserStore = context.UserStore context.recurly.configure(gon.global.recurly_public_api_key) window.configuredRecurly = true + componentDidMount: () -> LocationActions.load() @@ -100,7 +111,16 @@ UserStore = context.UserStore countryJsx = ` ` - `
+ plan_codes = [``] + for plan in gon.global.subscription_codes + plan_codes.push(``) + + plansJsx = ` + ` + + `
+

Update Payment

+
@@ -125,14 +145,16 @@ UserStore = context.UserStore {countryJsx} + + {plansJsx}
-
- - ` + + +
` }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee b/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee new file mode 100644 index 000000000..71e9fa931 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SubscriptionConcern.js.jsx.coffee @@ -0,0 +1,83 @@ +context = window + +@SubscriptionConcern = React.createClass({ + displayName: 'SubscriptionConcern' + + getTimeRemaining: (t) -> + + if t < 0 + t = -t + + seconds = Math.floor( (t/1000) % 60 ); + minutes = Math.floor( (t/1000/60) % 60 ); + hours = Math.floor( (t/(1000*60*60)) % 24 ); + days = Math.floor( t/(1000*60*60*24) ); + + return { + 'total': t, + 'days': days, + 'hours': hours, + 'minutes': minutes, + 'seconds': seconds + }; + + + displayTime: () -> + if false + # offset time by 10 minutes to get the 'you need to wait message' in + untilTime = @getTimeRemaining(@props.subscription.until.total + (10 * 60 * 1000)) + else + untilTime = @props.subscription.until + timeString = '' + if untilTime.days != 0 + timeString += "#{untilTime.days} days, " + if untilTime.hours != 0 || timeString.length > 0 + timeString += "#{untilTime.hours} hours, " + if untilTime.minutes != 0 || timeString.length > 0 + timeString += "#{untilTime.minutes} minutes, " + if untilTime.seconds != 0 || timeString.length > 0 + timeString += "#{untilTime.seconds} seconds" + + if timeString == '' + 'now!' + timeString + + openBrowserToPayment: (e) -> + context.JK.popExternalLink("/client#/account/subscription", true) + e.stopPropagation(); + + openBrowserToPlanComparison: (e) -> + context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-") + e.stopPropagation(); + return 'noclose' + + + render: () -> + content = null + if @props.subscription.until.total < 2000 + content = `
+

You have run out of time.

+

You can upgrade your plan to continue playing.

+
` + 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.

+
` + else + content = `
+

You will run out of monthly play time in:

+

{this.displayTime()}

+

You can upgrade your plan to continue playing.

+
` + + if content? + `
+ {content} +
` + else + `
` + +}) diff --git a/web/app/assets/javascripts/react-components/TopMessageHolder.js.jsx.coffee b/web/app/assets/javascripts/react-components/TopMessageHolder.js.jsx.coffee index 488f100d8..88af1176b 100644 --- a/web/app/assets/javascripts/react-components/TopMessageHolder.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/TopMessageHolder.js.jsx.coffee @@ -6,14 +6,13 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; @TopMessageHolder = React.createClass( { displayName: 'Top Message Holder', - - mixins: [Reflux.listenTo(ConfigStore, "onConfig")] + minimum_time_until_sub_prompt: 1000 * 60 * 30 # 30 minutes + mixins: [Reflux.listenTo(ConfigStore, "onConfig"), Reflux.connect(context.JK.Stores.Broadcast, 'notification')] getInitialState: () -> {} onConfig: (configs) -> - if configs.top_message @setState({top_message: configs.top_message}) @@ -23,7 +22,12 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; return false render: () -> - if @state.top_message + # only show the subscription concern message if there is a concerpn and due time is less thant 30 minutes away + if @state.notification && @state.notification.subscriptionConcern? && @state.notification.subscriptionConcern.until.total < @minimum_time_until_sub_prompt + `
+ +
` + else if @state.top_message `
diff --git a/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee new file mode 100644 index 000000000..b06291f16 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SubscriptionActions.js.coffee @@ -0,0 +1,8 @@ +context = window + +@SubscriptionActions = Reflux.createActions({ + updateSubscription: {} + changeSubscription: {} + cancelSubscription: {} + updatePayment: {} +}) diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee index 8db01b34d..247abb3c3 100644 --- a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -2,7 +2,7 @@ context = window @SessionHelper = class SessionHelper - constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack, preppingVstEnable) -> + constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack, preppingVstEnable, sessionRules, subscriptionRules) -> @app = app @session = session @participantsEverSeen = participantsEverSeen @@ -10,6 +10,8 @@ context = window @downloadingJamTrack = downloadingJamTrack @preppingVstEnable = preppingVstEnable @isLesson = @session?.lesson_session? + @sessionRules = sessionRules + @subscriptionRules = subscriptionRules if @isLesson @lessonId = @session.lesson_session.id 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 1d5968687..6c98178c2 100644 --- a/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/BroadcastStore.js.coffee @@ -2,6 +2,7 @@ $ = jQuery context = window logger = context.JK.logger broadcastActions = @BroadcastActions +SessionActions = @SessionActions rest = context.JK.Rest() @@ -17,6 +18,10 @@ BroadcastStore = Reflux.createStore( currentLessonTimer: null teacherFault: false isJamClass: false + sessionRules: null + subscriptionRules: null + subscriptionConcern: null + currentSubscriptionTimer: null init: -> this.listenTo(context.AppStore, this.onAppInit) this.listenTo(context.SessionStore, this.onSessionChange) @@ -29,6 +34,45 @@ BroadcastStore = Reflux.createStore( @timeManagement() @changed() + subscriptionTick: () -> + @subscriptionManagement() + @changed() + + 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' + + subscriptionManagement: () -> + @subscriptionConcern.until = @lessonUtils.getTimeRemaining(@subscriptionConcern.main_concern_time) + if @subscriptionConcern.until.total < -15000 + leaveBehavior = + location: "/client#/findSession" + 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())) + }) + + if @subscriptionConcern.main_concern_type == "remaining_month_play_time" + context.JK.Banner.show({ + title: "Out of Time For This Month", + html: context._.template($('#template-no-remaining-month-play-time').html(), {}, { variable: 'data' }), + buttons: buttons}); + else + context.JK.Banner.show({ + title: "Out of Time For This Session", + html: context._.template($('#template-no-remaining-session-play-time').html(), {}, { variable: 'data' }), + buttons: buttons}); + + SessionActions.leaveSession.trigger(leaveBehavior) + + timeManagement: () -> lastCheck = $.extend({}, @currentLesson) lessonSession = @currentLesson @@ -61,7 +105,7 @@ BroadcastStore = Reflux.createStore( logger.debug("BroadcastStore: lesson is over") @currentLesson.completed = true @currentLesson.success = @analysis.success - @clearTimer() + @clearLessonTimer() @changed() else if @analysis.analysis.reason != 'teacher_fault' logger.debug("BroadcastStore: not teacher fault; clearing lesson info") @@ -69,21 +113,31 @@ BroadcastStore = Reflux.createStore( else logger.debug("BroadcastStore: teacher is at fault") @teacherFault = true - @clearTimer() + @clearLessonTimer() @changed() clearLesson: () -> if @currentLesson? @currentLesson = null - @clearTimer() + @clearLessonTimer() @teacherFault = false @changed() - clearTimer: () -> + clearSubscription: () -> + @subscriptionConcern = null + @clearSubscriptionTimer() + @changed() + + clearLessonTimer: () -> if @currentLessonTimer? clearInterval(@currentLessonTimer) @currentLessonTimer = null + clearSubscriptionTimer: () -> + if @currentSubscriptionTimer? + clearInterval(@currentSubscriptionTimer) + @currentSubscriptionTimer = null + onNavChange: (nav) -> path = nav.screenPath.toLowerCase() @isJamClass = path.indexOf('jamclass') > -1 || path.indexOf('teacher') > -1 @@ -93,29 +147,59 @@ BroadcastStore = Reflux.createStore( @session = session currentSession = session.session - if currentSession? && currentSession.lesson_session? && session.inSession() + if currentSession? && session.inSession() + @subscriptionRules = session.subscriptionRules + @sessionRules = session.sessionRules - @currentSession = currentSession + if @subscriptionRules - lessonSession = currentSession.lesson_session - # so that receivers can check type of info coming at them via one-way events - lessonSession.isLesson = true + if !@subscriptionRules.remaining_month_until? && !@sessionRules.remaining_session_until? + console.log("no license issues") + @subscriptionConcern = null + else + @subscriptionConcern = {} + if !@subscriptionRules.remaining_month_until? + @subscriptionConcern.main_concern_time = @sessionRules.remaining_session_until + @subscriptionConcern.main_concern_type = "remaining_session_play_time" + else if !@sessionRules.remaining_session_play_time? + @subscriptionConcern.main_concern_time = @subscriptionRules.remaining_month_until + @subscriptionConcern.main_concern_type = "remaining_month_play_time" + else + if @sessionRules.remaining_session_play_time < @subscriptionRules.remaining_month_play_time + @subscriptionConcern.main_concern_time = @sessionRules.remaining_session_until + @subscriptionConcern.main_concern_type = "remaining_session_play_time" + else + @subscriptionConcern.main_concern_time = @subscriptionRules.remaining_month_until + @subscriptionConcern.main_concern_type = "remaining_month_play_time" - if lessonSession.status == 'completed' - lessonSession.completed = true - lessonSession.success = lessonSession.success - #else - # rest.getLessonAnalysis({id: lessonSession.id}).done((response) => @lessonAnalysisDone(response)).fail(@app.ajaxError) + @subscriptionManagement() + #logger.debug("BroadcastStore: session can play until: ", @subscriptionConcern.until, @subscriptionConcern.main_concert_time) + if !@currentSubscriptionTimer? + @currentSubscriptionTimer= setInterval((() => @subscriptionTick()), 1000) + @changed() - @currentLesson = lessonSession - @timeManagement() - logger.debug("BroadcastStore: currentLesson until: ", @currentLesson.until, lessonSession.scheduled_start) - if !@currentLessonTimer? - @currentLessonTimer = setInterval((() => @lessonTick()), 1000) - @changed() + if currentSession.lesson_session? + @currentSession = currentSession + lessonSession = currentSession.lesson_session + # so that receivers can check type of info coming at them via one-way events + lessonSession.isLesson = true + + if lessonSession.status == 'completed' + lessonSession.completed = true + lessonSession.success = lessonSession.success + #else + # rest.getLessonAnalysis({id: lessonSession.id}).done((response) => @lessonAnalysisDone(response)).fail(@app.ajaxError) + + @currentLesson = lessonSession + @timeManagement() + logger.debug("BroadcastStore: currentLesson until: ", @currentLesson.until, lessonSession.scheduled_start) + if !@currentLessonTimer? + @currentLessonTimer = setInterval((() => @lessonTick()), 1000) + @changed() else @clearLesson() + @clearSubscription() onLoad: () -> logger.debug("loading broadcast notification...") @@ -136,21 +220,10 @@ BroadcastStore = Reflux.createStore( @changed() changed: () -> - if @currentLesson? - @currentLesson.isStudent == @currentLesson.student_id == context.JK.currentUserId - @currentLesson.isTeacher = !@currentLesson.isStudent - @currentLesson.teacherFault = @teacherFault - @currentLesson.teacherPresent = @session.findParticipantByUserId(@currentLesson.teacher_id) - @currentLesson.studentPresent = @session.findParticipantByUserId(@currentLesson.student_id) - if (@currentLesson.teacherPresent? && @currentLesson.isStudent) || (@currentLesson.studentPresent? && @currentLesson.isTeacher) - # don't show anything if the other person is there - this.trigger(null) - else - this.trigger(@currentLesson) - else if @isJamClass - this.trigger({isJamClass: true}) + if @subscriptionConcern? + this.trigger({subscriptionConcern: @subscriptionConcern}) else - this.trigger(@broadcast) + this.trigger(null) } ) 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 5c8065d73..11db9980c 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -21,6 +21,8 @@ ConfigureTracksActions = @ConfigureTracksActions currentSessionId: null currentSession: null currentOrLastSession: null + sessionRules: null + subscriptionRules: null startTime: null currentParticipants: {} participantsEverSeen: {} @@ -53,7 +55,7 @@ ConfigureTracksActions = @ConfigureTracksActions @sessionUtils = context.JK.SessionUtils @recordingModel = new context.JK.RecordingModel(@app, rest, context.jamClient); RecordingActions.initModel(@recordingModel) - @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?) + @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?, @sessionRules, @subscriptionRules) onSessionJoinedByOther: (payload) -> clientId = payload.client_id @@ -78,7 +80,7 @@ ConfigureTracksActions = @ConfigureTracksActions onVideoChanged: (@videoState) -> issueChange: () -> - @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?) + @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?, @sessionRules, @subscriptionRules) this.trigger(@helper) onWindowBackgrounded: () -> @@ -726,6 +728,13 @@ ConfigureTracksActions = @ConfigureTracksActions deferred.resolve(); deferred + 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' + joinSession: () -> context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); @@ -816,13 +825,44 @@ ConfigureTracksActions = @ConfigureTracksActions @app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')) else if response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"] - leaveBehavior = location: "/client#/findSession" notify: title: "Unable to Join Session" text: "The session is currently recording." SessionActions.leaveSession.trigger(leaveBehavior) + else if response["errors"] && response["errors"]["remaining_session_play_time"] + leaveBehavior = + location: "/client#/findSession" + 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: "Out of Time For This Session", + html: context._.template($('#template-no-remaining-session-play-time').html(), {}, { variable: 'data' }), + buttons: buttons}); + SessionActions.leaveSession.trigger(leaveBehavior) + else if response["errors"] && response["errors"]["remaining_month_play_time"] + leaveBehavior = + location: "/client#/findSession" + 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: "Out of Time for the Month", + html: context._.template($('#template-no-remaining-month-play-time').html(), {}, { variable: 'data' }), + buttons: buttons}); + SessionActions.leaveSession.trigger(leaveBehavior) else @app.notifyServerError(xhr, 'Unable to Join Session'); else @@ -1051,6 +1091,30 @@ ConfigureTracksActions = @ConfigureTracksActions if sessionData != null @currentOrLastSession = sessionData + if sessionData.session_rules + @sessionRules = sessionData.session_rules + # TESTING: + @sessionRules.remaining_session_play_time = 60 * 30 + 15 # 30 minutes and 15 seconds + + # compute timestamp due time + if @sessionRules.remaining_session_play_time? + until_time = new Date() + until_time = new Date(until_time.getTime() + @sessionRules.remaining_session_play_time * 1000) + @sessionRules.remaining_session_until = until_time + + + if sessionData.subscription_rules + @subscriptionRules = sessionData.subscription_rules + # TESTING: + #@subscriptionRules.remaining_month_play_time = 60 * 30 + 15 # 30 minutes and 15 seconds + + if @subscriptionRules.remaining_month_play_time? + until_time = new Date() + until_time = new Date(until_time.getTime() + @subscriptionRules.remaining_month_play_time * 1000) + #until_time.setSeconds(until_time.getSeconds() + @subscriptionRules.remaining_month_play_time) + @subscriptionRules.remaining_month_until = until_time + + @currentSession = sessionData if context.jamClient.UpdateSessionInfo? @@ -1078,6 +1142,7 @@ ConfigureTracksActions = @ConfigureTracksActions # called by anyone wanting to leave the session with a certain behavior onLeaveSession: (behavior) -> logger.debug("attempting to leave session", behavior) + if behavior.notify @app.layout.notify(behavior.notify) @@ -1206,6 +1271,8 @@ ConfigureTracksActions = @ConfigureTracksActions @joinDeferred = null @isRecording = false @currentSessionId = null + @sessionRules = null + @subscriptionRules = null @currentParticipants = {} @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []} @openBackingTrack = null diff --git a/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee new file mode 100644 index 000000000..a36a92a84 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SubscriptionStore.js.coffee @@ -0,0 +1,66 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@SubscriptionStore = Reflux.createStore( + { + listenables: @SubscriptionActions + + subscription: null + + + init: -> + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (@app) -> + + onUpdateSubscription: (subscription) -> + + rest.getSubscription().done (subscription) => + @subscription = subscription + console.log("subscription store update", subscription) + @trigger(@subscription) + .fail(() => + @app.layout.notify({ + 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}" + }) + ) + + onCancelSubscription: () -> + rest.cancelSubscription().done((result) => + @subscription = {} + console.log("cancelled successfully") + @trigger(@subscription) + ) + .fail((jqXHR) => + @app.layout.notify({ + title: "Subscription Cancelled", + text: "Thanks for being a supporter!" + }) + ) + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 755a7ce1b..cecd6d0e8 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -1209,7 +1209,7 @@ $links.on('click', popOpenBrowser); } - context.JK.popExternalLink = function (href) { + context.JK.popExternalLink = function (href, login) { if (!context.jamClient) { return; } @@ -1220,11 +1220,28 @@ href = window.location.protocol + '//' + window.location.host + href; } + if(login) { + var rememberToken = $.cookie("remember_token"); + if(rememberToken) { + href = window.location.protocol + '//' + window.location.host + "/passthrough?redirect-to=" + encodeURIComponent(href) + '&stoken=' + encodeURIComponent(rememberToken) + } + + } context.jamClient.OpenSystemBrowser(href); } return false; } + context.JK.popExternalLinkAndLogin = function(href) { + + var rememberToken = $.cookie("remember_token"); + + context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-") + if(rememberToken) { + "https://" + } + } + context.JK.checkbox = function ($checkbox, dark) { if (dark){ return $checkbox.iCheck({ diff --git a/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss b/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss index 0f949ee02..02b05d07a 100644 --- a/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss +++ b/web/app/assets/stylesheets/client/react-components/AccountSubscriptionScreen.scss @@ -18,13 +18,29 @@ padding-top: 20px; } - #subscription-account-data { + #subscription-account-data, #subscription-elements { display:grid; - display: grid; align-items: center; - justify-content: center; + #justify-content: center; grid-template-columns: 50% 50%; } + + #subscription-form { + + } + + #change-subscription-form { + max-width:35rem; + display:grid; + align-items: center; + #justify-content: center; + grid-template-columns: 8rem 8rem 8rem; + } + + .payment-block { + margin-top:20px; + } + form { max-width:25rem; } @@ -63,7 +79,7 @@ font-size: 1rem; font-weight: bold; box-shadow: none; - border-radius: 0; + #border-radius: 0; color: black; -webkit-appearance: none; -webkit-transition: border-color 0.3s; @@ -140,4 +156,9 @@ margin-bottom: 15px; } + h3 { + font-weight:bold; + margin:30px 0 20px; + } + } diff --git a/web/app/assets/stylesheets/client/react-components/broadcast.scss b/web/app/assets/stylesheets/client/react-components/broadcast.scss index ce2cd17b1..c327a17bf 100644 --- a/web/app/assets/stylesheets/client/react-components/broadcast.scss +++ b/web/app/assets/stylesheets/client/react-components/broadcast.scss @@ -45,6 +45,19 @@ width:100%; } } + + &.subscription { + border-width:0; + p { + text-align:center; + color:$ColorTextTypical; + margin-bottom:10px; + } + .message { + width:100%; + font-size:18px; + } + } } .message { float:left; diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index 37bb21c52..c0e8f0e1a 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -339,12 +339,29 @@ class ApiMusicSessionsController < ApiController params[:parent_client_id] ) + if @connection.errors.any? response.status = :unprocessable_entity respond_with @connection else - @music_session = @connection.music_session + + # used in rabl to render extra data + @on_join = true + @subscription_rules = current_user.subscription_rules + @session_rules = { remaining_session_play_time: @music_session.play_time_remaining(current_user) } + + # check if the user has gone past acceptable play time + if !@session_rules[:remaining_session_play_time].nil? && @session_rules[:remaining_session_play_time] <= 0 + # user has no session time for this session left. + render :json => { :errors => {:remaining_session_play_time => ['none remaining']}}, :status => 422 + return + elsif !@subscription_rules[:remaining_month_play_time].nil? && @subscription_rules[:remaining_month_play_time] <= 0 + # user has no session time this month. + render :json => { :errors => {:remaining_month_play_time=> ['none remaining']}}, :status => 422 + return + end + respond_with @music_session, responder: ApiResponder, :status => 201, :location => api_session_detail_url(@connection.music_session) end diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index e3d31491a..1062e4fba 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -124,7 +124,7 @@ class ApiRecurlyController < ApiController render json: {message: x.inspect, errors: x.errors}, :status => 404 end - def create_subscriptions + def create_subscription begin sale = Sale.purchase_subscription(current_user, params[:recurly_token], params[:plan_code]) subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) @@ -135,11 +135,10 @@ class ApiRecurlyController < ApiController end def get_subscription - subscription_id = current_user.recurly_subscription_id - subscription = Recurly::Subscription.find(subscription_id) if subscription_id + subscription = @client.find_subscription(current_user) if subscription - render :json => subscription + render :json => subscription.to_json else render :json => {} end @@ -148,8 +147,13 @@ class ApiRecurlyController < ApiController def cancel_subscription begin @client.cancel_subscription(current_user.recurly_subscription_id) - subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) - render :json => subscription.to_json + subscription = @client.find_subscription(current_user) + + if subscription + render :json => subscription.to_json + else + render :json => {} + end rescue RecurlyClientError => x render json: {:message => x.inspect, errors: x.errors}, :status => 404 end @@ -157,9 +161,14 @@ class ApiRecurlyController < ApiController def change_subscription_plan begin - @client.change_subscription_plan(current_user.recurly_subscription_id, params[:plan_code]) - subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) - render :json => subscription.to_json + result = @client.change_subscription_plan(current_user, params[:plan_code]) + 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 + end + rescue RecurlyClientError => x render json: {:message => x.inspect, errors: x.errors}, :status => 404 end @@ -167,7 +176,7 @@ class ApiRecurlyController < ApiController def change_subscription_payment begin - @client.change_subscription_payment(current_user.recurly_subscription_id, params[:recurly_token], params[:billing_ifo]) + @client.change_subscription_payment(current_user.recurly_subscription_id, params[:recurly_token], params[:billing_info]) subscription = Recurly::Subscription.find(current_user.recurly_subscription_id) render :json => subscription.to_json rescue RecurlyClientError => x diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index d3b2fcc6b..ed9560414 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -494,13 +494,13 @@ class ApiUsersController < ApiController limit = 20 if limit <= 0 offset = params[:offset].to_i offset = 0 if offset < 0 - @notifications = Notification.where(description: 'TEXT_MESSAGE').where('(source_user_id = (?) AND target_user_id = (?)) OR (source_user_id = (?) AND target_user_id = (?))', @user.id, receiver_id, receiver_id, @user.id).offset(offset).limit(limit).order('created_at DESC') + @notifications = Notification.joins(:source_user).where(description: 'TEXT_MESSAGE').where('(source_user_id = (?) AND target_user_id = (?)) OR (source_user_id = (?) AND target_user_id = (?))', @user.id, receiver_id, receiver_id, @user.id).offset(offset).limit(limit).order('created_at DESC') else limit = params[:limit].to_i limit = 20 if limit <= 0 offset = params[:offset].to_i offset = 0 if offset < 0 - @notifications = @user.notifications.offset(offset).limit(limit) + @notifications = @user.notifications.joins(:source_user).offset(offset).limit(limit) end respond_with @notifications, responder: ApiResponder, :status => 200 diff --git a/web/app/controllers/sessions_controller.rb b/web/app/controllers/sessions_controller.rb index 0dbc227e3..252e9f1b4 100644 --- a/web/app/controllers/sessions_controller.rb +++ b/web/app/controllers/sessions_controller.rb @@ -326,6 +326,17 @@ class SessionsController < ApplicationController render :json => {has_google_auth: (!!current_user && !!UserAuthorization.google_auth(current_user).first)} end + def passthrough + if params['stoken'] + # should be a remember_me cookie. log them in and redirect + user = User.find_by_remember_token(params['stoken']) + if !user.nil? + sign_in user + end + end + redirect_after_signin('/') + end + def redirect_after_signin(default) redirect_to(params['redirect-to'].blank? ? default : params['redirect-to']) end diff --git a/web/app/helpers/music_session_helper.rb b/web/app/helpers/music_session_helper.rb index 91d9f2b74..c91dd7a15 100644 --- a/web/app/helpers/music_session_helper.rb +++ b/web/app/helpers/music_session_helper.rb @@ -93,4 +93,6 @@ module MusicSessionHelper def timezone_list(options) select_tag('timezone-list', timezone_options, options) end + + end diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index ee1fc8410..b0c5a2544 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -15,6 +15,16 @@ 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| + @subscription_rules + end + + node :session_rules do |session| + @session_rules + end + end + node :can_join do |session| session.can_join?(current_user, true) end @@ -53,7 +63,7 @@ else attributes :ip_address, :client_id, :joined_session_at, :audio_latency, :id, :metronome_open, :is_jamblaster, :client_role, :parent_client_id, :client_id_int node :user do |connection| - { :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state } + { :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state, :subscription => connection.user.subscription_plan_code } end child(:tracks => :tracks) { diff --git a/web/app/views/clients/_account_subscription.html.slim b/web/app/views/clients/_account_subscription.html.slim index 37a224c81..fe96c3903 100644 --- a/web/app/views/clients/_account_subscription.html.slim +++ b/web/app/views/clients/_account_subscription.html.slim @@ -1,5 +1,5 @@ #account-subscription.screen.secondary layout="screen" layout-id="account/subscription" - .content-head + .content-head{style="height:26px"} .content-icon = image_tag "content/icon_account.png", :size => "27x20" h1 diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 637535498..ce9686384 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -276,6 +276,18 @@ script type="text/template" id="template-help-jamtrack-controls-disabled" script type="text/template" id="template-help-volume-media-mixers" | Audio files only expose both master and personal mix controls, so any change here will also affect everyone in the session. +script type="text/template" id="template-no-remaining-session-play-time" + .no-remaining-session-play-time + p There is no more time left in this session. You can upgrade your plan. + +script type="text/template" id="template-no-remaining-month-play-time" + .no-remaining-monthly-play-time + p There is no more session time left for this month. You can upgrade your plan. + +script type="text/template" id="template-session-too-big-kicked" + .session-too-big-kicked + p Due to your current plan, you were forced to leave the session because someone else joined the session just now. You can upgrade your plan. + script type="text/template" id="template-help-downloaded-jamtrack" .downloaded-jamtrack p When a JamTrack is first purchased, a user-specific version of it is created on the server. Once it's ready, it's then downloaded to the client. diff --git a/web/config/application.rb b/web/config/application.rb index b8b87255d..d17c1ef02 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -482,6 +482,6 @@ 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}] end end diff --git a/web/config/initializers/gon.rb b/web/config/initializers/gon.rb index 4eb36c68d..b60748b9a 100644 --- a/web/config/initializers/gon.rb +++ b/web/config/initializers/gon.rb @@ -26,4 +26,5 @@ Gon.global.vst_enabled = Rails.application.config.vst_enabled Gon.global.chat_opened_by_default = Rails.application.config.chat_opened_by_default Gon.global.network_test_required = Rails.application.config.network_test_required Gon.global.musician_count = Rails.application.config.musician_count +Gon.global.subscription_codes = Rails.application.config.subscription_codes Gon.global.env = Rails.env diff --git a/web/config/routes.rb b/web/config/routes.rb index 610fc1fee..da504ac93 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -27,6 +27,7 @@ Rails.application.routes.draw do get '/signin', to: 'sessions#signin' post '/signin', to: 'sessions#create' delete '/signout', to: 'sessions#destroy' + get '/passthrough', to: 'sessions#passthrough' match '/redeem_giftcard', to: 'landings#redeem_giftcard', via: :get match '/account/activate/code_old', to: 'landings#account_activate', via: :get @@ -399,6 +400,11 @@ Rails.application.routes.draw do match '/recurly/update_billing_info' => 'api_recurly#update_billing_info', :via => :put match '/recurly/place_order' => 'api_recurly#place_order', :via => :post match '/ios/order_placed' => 'api_jam_tracks#ios_order_placed', :via => :post + match '/recurly/create_subscription' => 'api_recurly#create_subscription', :via => :post + match '/recurly/get_subscription' => 'api_recurly#get_subscription', :via => :get + match '/recurly/change_subscription' => 'api_recurly#change_subscription_plan', :via => :post + match '/recurly/cancel_subscription' => 'api_recurly#cancel_subscription', :via => :post + match '/recurly/change_subscription_payment' => 'api_recurly#change_subscription_payment', :via => :post # paypal match '/paypal/checkout/detail' => 'api_pay_pal#checkout_detail', :via => :post diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 062050a6d..31cbe51ac 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -445,7 +445,7 @@ module JamWebsockets # a unique ID for this TCP connection, to aid in debugging client.channel_id = handshake.query["channel_id"] - @log.debug "client connected #{client} with channel_id: #{client.channel_id}" + @log.debug "client connected #{client} with channel_id: #{client.channel_id} Original-IP: #{handshake.headers["X-Forwarded-For"]}" # check for '?pb' or '?pb=true' in url query parameters