Compare commits

..

77 Commits

Author SHA1 Message Date
Seth Call dfe5facad6 ci: add simple test workflow
Test Runner / test (push) Waiting to run Details
Build Admin / build (push) Waiting to run Details
Environment Orchestrator / orchestrate (push) Waiting to run Details
2026-03-10 19:32:55 -05:00
Seth Call abae34c108 ci: switch to custom dagger runner label
Build Admin / build (push) Failing after 10s Details
Environment Orchestrator / orchestrate (push) Failing after 7s Details
2026-03-10 19:21:45 -05:00
Seth Call df8cf11e37 ci: remove paths filter to force build on mirror sync
Build Admin / build (push) Failing after 12s Details
Environment Orchestrator / orchestrate (push) Failing after 8s Details
2026-03-10 19:17:37 -05:00
Seth Call 7baf003b90 ci: touch admin file to bypass paths filter
Build Admin / build (push) Failing after 10s Details
Environment Orchestrator / orchestrate (push) Failing after 7s Details
2026-03-10 19:12:50 -05:00
Seth Call 1944602b97 ci: fix gitea token reference and checkout action url
Build Admin / build (push) Failing after 19s Details
Environment Orchestrator / orchestrate (push) Failing after 9s Details
2026-03-10 19:03:44 -05:00
Seth Call 86e151d239 ci: add workflow_dispatch to allow manual triggers on mirrors
Build Admin / build (push) Failing after 39s Details
Environment Orchestrator / orchestrate (push) Failing after 17s Details
2026-03-10 18:57:08 -05:00
Seth Call 6a1506bbe8 ci: add action workflow to develop to enable branch triggers
Build Admin / build (push) Failing after 46s Details
Environment Orchestrator / orchestrate (push) Failing after 18s Details
2026-03-10 18:37:32 -05:00
Seth Call 64a93dd42f support beta 2026-02-28 20:39:34 -06:00
Seth Call eb52813822 defaut beta to true 2026-02-21 08:56:52 -06:00
Seth Call 8613a03d00 ok fix the API more 2026-02-16 09:05:12 -06:00
Seth Call 8f0b8929ba ars apis 2026-02-15 22:04:17 -06:00
Seth Call 08e1c5274d track utm_id 2026-02-04 21:53:19 -06:00
Seth Call 5f347ccfac handle All 2026-02-04 21:31:23 -06:00
Seth Call ca2bf19b7a cleanup query 2026-02-04 20:58:16 -06:00
Seth Call fad1f9d6d9 Add paid = cpc on jammers cohots 2026-02-04 19:53:00 -06:00
Seth Call 51838fb413 Remove test drive 2026-02-03 19:05:12 -06:00
Seth Call 45cb401112 Controls for user source 2026-02-01 14:15:41 -06:00
Seth Call 9078515984 user source update 2026-01-31 12:20:37 -06:00
Seth Call 52ae83e2f4 Addtocart instead 2026-01-31 12:15:26 -06:00
Seth Call b454cf9ead AddToCart 2026-01-28 20:04:40 -06:00
Seth Call 65f5624ff9 add array support for facebook ad source 2026-01-24 00:02:44 -06:00
Seth Call e82450dfe9 Support more utm tracking 2026-01-23 23:17:35 -06:00
Seth Call f1992eaa78 build this 2026-01-22 22:36:22 -06:00
Seth Call bdeecc76c6 fix reporting 2026-01-22 21:21:17 -06:00
Seth Call 8c8024c12b event test code 2026-01-19 13:48:26 -06:00
Seth Call c183af3d6b remove rails logger ref 2026-01-19 13:21:26 -06:00
Seth Call ab424c21d2 use puts 2026-01-19 12:49:37 -06:00
Seth Call 753f35b24d click signup redirects to the right place in jam-ui 2026-01-19 12:34:30 -06:00
Seth Call 624853c868 Store UTM cookies 2026-01-16 22:15:36 -06:00
Seth Call 71d8571eb9 JKLayout adding Meta Tracking 2026-01-16 16:44:46 -06:00
Seth Call fae16c7483 better logging on CAPI 2026-01-15 08:00:34 -06:00
Seth Call 253a6c566b Bettor logging 2026-01-15 07:59:27 -06:00
Seth Call ed561be4b5 meta_tracking included 2026-01-14 19:56:42 -06:00
Seth Call eb69640667 fix utm source 2026-01-14 14:54:56 -06:00
Seth Call 9b0a9d1f32 cleanup around app-config 2026-01-14 14:42:22 -06:00
Seth Call e1cc5483a2 facebook pixel id and access token 2026-01-14 12:17:39 -06:00
Seth Call 73d2a7a020 more changes for event rework 2026-01-14 10:30:33 -06:00
Seth Call eb298d6859 fix 2026-01-14 09:20:57 -06:00
Seth Call 93c4154648 add capi transmitter and missing event type 2026-01-14 08:58:37 -06:00
Seth Call fbd871d204 reviewing 2026-01-14 08:52:19 -06:00
Seth Call 0d82f6ee16 allow video tokens to last a long time 2026-01-08 06:03:58 -06:00
Seth Call 1279b16ec0 Update manifest maker 2025-10-26 14:33:00 -05:00
Seth Call e3cff0a825 Uncomment TrialReminders 2025-10-18 17:40:46 -05:00
Nuwan Chaturanga 6a6e4cde09 Merged in fix_plg_email_delivery_timeing_issue (pull request #70)
fix PLG email timing

Approved-by: Seth Call
2025-10-17 13:16:50 +00:00
Seth Call 1dd15fb0aa Add tests for all PLG emails 2025-10-17 08:15:46 -05:00
Seth Call 9282369e54 VRFS-5690 - tag jamtrack and jammers separetly on admin report - fix jammers view 2025-10-13 15:23:01 -05:00
Seth Call 828191d683 VRFS-5690 - tag jamtrack and jammers separetly on admin report - fix jammers view 2025-10-13 15:16:41 -05:00
Seth Call c0031cfe3d VRFS-5690 - tag jamtrack and jammers separetly on admin report 2025-10-13 14:33:45 -05:00
Seth Call 9b17546082 omit trial send reminders for now 2025-10-13 12:02:44 -05:00
Seth Call eed3d51f4b VRFS-5691 - fix both the jamtrack flow and the inability to sign out - take 2 2025-10-13 11:44:17 -05:00
Seth Call 4eac68b645 VRFS-5691 - fix both the jamtrack flow and the inability to sign out 2025-10-12 11:55:50 -05:00
Nuwan fe6157e8cf ensure not to send email 2 and 3 tighltly behind email 1 in this sequence 2025-09-30 13:14:39 +05:30
Nuwan 675bf2b69c fix PLG email timing
do not send emails if the date has passed
2025-09-22 00:07:16 +05:30
Nuwan 4ffc0d9b3b Merge branch 'develop' of ssh://altssh.bitbucket.org:443/jamkazam/jam-cloud into develop 2025-08-20 20:36:18 +05:30
Nuwan 7b665325f7 unsubscribe/change email confirmation fixes 2025-08-20 20:34:52 +05:30
Nuwan eab17b3340 uncomment the lines
uncomment the lines which were disabled for debug purposes in the
prev. commit
2025-08-20 20:34:52 +05:30
Nuwan 4fafe30141 add more changes which were missed in prev. commit 2025-08-20 20:34:52 +05:30
Nuwan 7f85c91601 trail end reminderd
send emails when the trail perieod expired.
2025-08-20 20:34:52 +05:30
Nuwan d424026f17 fix payment method page element loading
this resolves the race condition issue when loading recurly payment element
and hence sometimes it wasn't showing
2025-08-20 20:31:44 +05:30
Nuwan Chaturanga a4f8935b3a Merged in fix_403_errors_in_public_pages (pull request #69)
unsubscribe/change email confirmation fixes

Approved-by: Seth Call
2025-08-19 16:19:12 +00:00
Nuwan Chaturanga 4f837ae67f Merged in 5662-trial_end_reminder_emails (pull request #68)
trail end reminderd

Approved-by: Seth Call
2025-08-19 16:07:57 +00:00
Nuwan 0053775c7e uncomment the lines
uncomment the lines which were disabled for debug purposes in the
prev. commit
2025-08-19 21:04:28 +05:30
Nuwan 9d6c71829f unsubscribe/change email confirmation fixes 2025-08-19 20:39:39 +05:30
Nuwan d00a0c08f7 add more changes which were missed in prev. commit 2025-08-19 02:13:16 +05:30
Nuwan 7e2c917ca0 trail end reminderd
send emails when the trail perieod expired.
2025-08-19 00:07:09 +05:30
Nuwan Chaturanga a84a55f178 Merged in 5661-PLG-play-with-others (pull request #67)
PLG email for 2+ session

Approved-by: Seth Call
2025-08-16 23:50:48 +00:00
Nuwan 25ecab2c65 fix billing details + card data processing
remove saving billing address details to database in the first place
before sending to recurly. Now the backend handles address data saving
after success response from recurly.
2025-08-15 18:31:03 +05:30
Nuwan b2f344fd30 PLG email for 2+ session
email reminders for the users to have 2+ session
VRFS-5661
2025-08-14 22:06:18 +05:30
Nuwan 3bba9ec619 fix error in user_observer 2025-08-13 14:37:31 +05:30
Nuwan 86e03e0ba7 change email confirm page 2025-08-13 14:26:58 +05:30
Nuwan 67fd15c75c wip after changing email show confirmation page within new website 2025-08-13 13:51:54 +05:30
Nuwan b7a41c6465 unsubscribe page
add page in new website to be shown to the users
when they follow unsubscribe link (in email footer)
2025-08-06 20:30:12 +05:30
Nuwan a8d5b8e735 update in the front end showing user payment details persistance 2025-07-29 22:37:54 +05:30
Nuwan f977b7298e fix error when showing if user has stored card
fix the issue of showing this information incorrectly on payment method
page. also this comment disables front end validation of card details
2025-07-28 18:47:02 +05:30
Seth Call 9a41e8a236 Fix cutoff_date not being passed in and requirees of TestGearSetup 2025-07-25 20:06:47 -05:00
Nuwan Chaturanga 3d113e3877 Merged in 5646-test_session_reminder (pull request #66)
PLG feature for Test Session reminder emails

* PLG feature for Test Session reminder emails

Implement the Test Session reminder emails VRFS-5646

* cutoff date for test gear reminder email

add cutoff date config to limit the selection after a certain date
add batch_size:100 to loops


Approved-by: Seth Call
2025-07-25 23:13:21 +00:00
Nuwan Chaturanga c7e80a0694 Merged in 5645-payment_method_page (pull request #65)
5645 payment method page

* wip payment method in new site

* payment method page

new page to add user's payment method (credit card / paypal) alone
with billing address details

* Update recurly/braintree tokens


Approved-by: Seth Call
2025-07-24 03:25:55 +00:00
103 changed files with 2955 additions and 36165 deletions

View File

@ -0,0 +1,25 @@
name: Build Admin
on:
push:
branches:
- develop
jobs:
build:
runs-on: dagger
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Install Dagger
run: |
curl -L https://dl.dagger.io/dagger/install.sh | sh
sudo mv bin/dagger /usr/local/bin/
- name: Login to Gitea Registry
run: echo "${{ gitea.token }}" | docker login git.staging.jamkazam.com -u ${{ gitea.actor }} --password-stdin
- name: Build and Publish with Dagger
working-directory: ./admin
run: |
dagger call build-local --source=. --repo-root=../ publish --address=git.staging.jamkazam.com/seth/jam-cloud-admin:latest

View File

@ -0,0 +1,41 @@
name: Environment Orchestrator
on: [push]
jobs:
orchestrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Component Deployment Gatekeeper
run: |
# JAM_CLUSTER_ENV should be set to 'staging' or 'production' in the Gitea Runner
ENV="${JAM_CLUSTER_ENV:-staging}"
echo "🌐 Cluster Environment: $ENV"
# 1. Extract modes for this environment
ADMIN_MODE=$(jq -r ".environments.$ENV.admin" .jk-deploy.json)
WEB_MODE=$(jq -r ".environments.$ENV.web" .jk-deploy.json)
WS_MODE=$(jq -r ".environments.$ENV.[\"websocket-gateway\"]" .jk-deploy.json)
# 2. Conditional Execution
if [ "$ADMIN_MODE" == "short-circuit" ]; then
echo "⚡ ADMIN: Short-circuit detected. Deploying immediately..."
cd admin && dagger call ship --source=.
else
echo "⏸️ ADMIN: Mode is $ADMIN_MODE. Skipping short-circuit deploy."
fi
if [ "$WEB_MODE" == "short-circuit" ]; then
echo "⚡ WEB: Short-circuit detected. Deploying immediately..."
cd web && dagger call ship --source=.
else
echo "⏸️ WEB: Mode is $WEB_MODE. Skipping short-circuit deploy."
fi
if [ "$WS_MODE" == "short-circuit" ]; then
echo "⚡ WS-GATEWAY: Short-circuit detected. Deploying immediately..."
cd websocket-gateway && just ship
else
echo "⏸️ WS-GATEWAY: Mode is $WS_MODE. Skipping short-circuit deploy."
fi

View File

@ -0,0 +1,8 @@
name: Test Runner
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Runner is working!"

View File

View File

@ -0,0 +1,185 @@
class Spacer
def self.spacer(val, row)
percentage = ((val * 100) / row.total.to_f).round(1).to_s
('%-5.5s' % percentage).gsub(' ', ' ') + '% - ' + val.to_s
end
end
=begin
select
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
select first_name, last_name, email
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
AND first_music_session_at is NULL;
=end
ActiveAdmin.register_page "Jammers Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "Jammers Subscription Cohorts" do
filter_type = params[:filter_type] || 'All'
filter_campaign = params[:filter_campaign]
filter_campaign_id = params[:filter_campaign_id]
filter_ad_set = params[:filter_ad_set]
filter_ad_name = params[:filter_ad_name]
campaigns = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_utm_campaign).compact.sort
campaign_ids = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_id).compact.sort
ad_sets = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_term).compact.sort
ad_names = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_content).compact.sort
div style: "margin-bottom: 20px; padding: 10px; background-color: #f4f4f4; border-radius: 4px;" do
form action: admin_jammers_subscription_cohorts_path, method: :get do
span "Source: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_type', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: 'All', selected: filter_type == 'All'
option "Organic", value: 'Organic', selected: filter_type == 'Organic'
option "Advertising", value: 'Advertising', selected: filter_type == 'Advertising'
end
if filter_type == 'Advertising'
div style: "margin-top: 10px;" do
span "Campaign Name: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_campaign', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_campaign == 'NULL'
campaigns.each do |c|
option c, value: c, selected: filter_campaign == c
end
end
end
div style: "margin-top: 10px;" do
span "Campaign ID: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_campaign_id', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_campaign_id == 'NULL'
campaign_ids.each do |c|
option c, value: c, selected: filter_campaign_id == c
end
end
end
div style: "margin-top: 10px;" do
span "Ad Set: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_ad_set', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_ad_set == 'NULL'
ad_sets.each do |c|
option c, value: c, selected: filter_ad_set == c
end
end
div style: "margin-top: 10px;" do
span "Ad Name: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_ad_name', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_ad_name == 'NULL'
ad_names.each do |c|
option c, value: c, selected: filter_ad_name == c
end
end
end
end
end
noscript { input type: :submit, value: "Filter" }
end
end
h2 "Users Grouped By Month as Paying Subscribers"
query = User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed,
count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{LEFT JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON TRUE })
if filter_type == 'Organic'
query = query.where("users.origin_utm_source = 'organic'")
elsif filter_type == 'Advertising'
query = query.where("origin_utm_medium = 'cpc'")
if filter_campaign.present?
if filter_campaign == 'NULL'
query = query.where("users.origin_utm_campaign IS NULL")
elsif filter_campaign != 'All'
query = query.where("users.origin_utm_campaign = ?", filter_campaign)
end
end
if filter_campaign_id.present?
if filter_campaign_id == 'NULL'
query = query.where("users.origin_id IS NULL")
elsif filter_campaign_id != 'All'
query = query.where("users.origin_id = ?", filter_campaign_id)
end
end
if filter_ad_set.present?
if filter_ad_set == 'NULL'
query = query.where("users.origin_term IS NULL")
elsif filter_ad_set != 'All'
query = query.where("users.origin_term = ?", filter_ad_set)
end
end
if filter_ad_name.present?
if filter_ad_name == 'NULL'
query = query.where("users.origin_content IS NULL")
elsif filter_ad_name != 'All'
query = query.where("users.origin_content = ?", filter_ad_name)
end
end
end
table_for query.group("date_trunc('month', users.created_at)")
.where("j.created_at IS NULL OR (j.created_at - users.created_at) >= INTERVAL '2 hours'")
.order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
column "Downloaded", Proc.new { |user| raw(Spacer.spacer(user.downloaded, user)) }
column "Ran Client", Proc.new { |user| raw(Spacer.spacer(user.ran_client, user)) }
column "FTUE", Proc.new { |user| raw(Spacer.spacer(user.ftue, user)) }
column "Any Session", Proc.new { |user| raw(Spacer.spacer(user.any_session, user)) }
column "2+ Session", Proc.new { |user| raw(Spacer.spacer(user.real_session, user)) }
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end
end
end

View File

@ -25,13 +25,12 @@ from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-0
=end
ActiveAdmin.register_page "Subscription Cohorts" do
ActiveAdmin.register_page "JamTrack Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "Subscription Cohorts" do
content :title => "JamTrack Subscription Cohorts" do
h2 "Users Grouped By Month as Paying Subscribers"
table_for User.select(%Q{date_trunc('month', created_at) as month,
table_for User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
@ -41,8 +40,21 @@ ActiveAdmin.register_page "Subscription Cohorts" do
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
}).group("date_trunc('month', created_at)").order("date_trunc('month', created_at) DESC") do |row|
count(first_subscribed_at) as subscribed,
count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{INNER JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON (j.created_at - users.created_at) < INTERVAL '2 hours' })
.group("date_trunc('month', users.created_at)").order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
@ -54,6 +66,7 @@ ActiveAdmin.register_page "Subscription Cohorts" do
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end
end

View File

@ -6,15 +6,18 @@ ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
config.batch_actions = false
config.clear_action_items!
config.filters = false
config.per_page = 250
scope("Most Recent First", default: true) { |scope| scope.unscoped.order('created_at desc')}
scope("Paid", default: true) { |scope| scope.unscoped.where(:origin_utm_medium => 'cpc').order('created_at desc') }
scope("Inorganic Source") { |scope| scope.unscoped.where("origin_utm_source != 'organic' OR origin_utm_source IS NULL").order('created_at desc') }
scope("Include Organic") { |scope| scope.unscoped.order('created_at desc') }
index do
column "Email" do |user|
user.email
end
column "Bought TestDrive" do |user|
!user.most_recent_test_drive_purchase.nil? ? "Yes" : "No"
column "Signup (CST)" do |user|
user.created_at.in_time_zone("Central Time (US & Canada)")
end
column "UTM Source" do |user|
user.origin_utm_source
@ -25,8 +28,23 @@ ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
column "UTM Campaign" do |user|
user.origin_utm_campaign
end
column "UTM ID" do |user|
user.origin_id
end
column "UTM Term" do |user|
user.origin_term
end
column "UTM Content" do |user|
user.origin_content
end
column "Referrer" do |user|
user.origin_referrer
end
column "FB Click ID" do |user|
user.facebook_click_id
end
column "FB Browser ID" do |user|
user.facebook_browser_id
end
end
end

View File

@ -2,55 +2,97 @@ class ArsesController < ApplicationController
respond_to :json
# create or update a client_artifact row
def get_or_create
name = params[:name]
provider = params[:provider]
active = params[:active]
ip = params[:ip]
username = params[:username]
password = params[:password]
topology = params[:topology]
ars_id = params[:ars_id]
puts "TOPOLOGY #{topology}"
if ars_id
ars = Ars.find_by_id_int(ars_id)
end
if ars.nil?
ars = Ars.new
ars.name = name
ars.id_int = ars_id if !ars_id.nil?
def index
if params[:code] != Rails.application.config.data_dump_code
render :json => {error: "Unauthorized"}, :status => 401
return
end
ars.provider = provider
ars.active = active
ars.ip = ip
ars.password = password
ars.username = username
if topology
ars.city = topology['city']
ars.country = topology['country']
ars.continent = topology['continent']
ars.latitude = topology['latitude']
ars.longitude = topology['longitude']
ars.subdivision = topology['subdivision']
end
ars.save
@arses = JamRuby::Ars.all
render :json => @arses
end
@ars = ars
unless @ars.errors.any?
if ars_id.nil?
ars.reload
ars_id = ars.id_int
def update
if params[:code] != Rails.application.config.data_dump_code
render :json => {error: "Unauthorized"}, :status => 401
return
end
begin
# Primary ID lookup
@ars = JamRuby::Ars.find_by_id(params[:id])
# Explicit secondary lookups if primary ID fails
@ars ||= JamRuby::Ars.find_by_id_int(params[:id_int]) if params[:id_int]
@ars ||= JamRuby::Ars.find_by_name(params[:name]) if params[:name]
if @ars.nil?
render :json => {error: "Not Found"}, :status => 404
return
end
@ars = Ars.find_by_id_int(ars_id)
render :json => {id_int: @ars.id_int, id: @ars.id, name: @ars.name, provider: @ars.provider, active: @ars.active, ip: @ars.ip}, :status => :ok
else
response.status = :unprocessable_entity
respond_with @ars
end
allowed = [:password, :username, :active, :beta, :name, :provider, :id_int, :ip, :port, :continent, :country, :city, :subdivision, :latitude, :longitude]
update_hash = {}
allowed.each do |attr|
update_hash[attr] = params[attr] if params.has_key?(attr)
end
if @ars.update_attributes(update_hash, as: :admin)
render :json => @ars, :status => :ok
else
render :json => @ars.errors, :status => :unprocessable_entity
end
rescue => e
render :json => {error: e.message, backtrace: e.backtrace.first(5)}, :status => 500
end
end
# create or update a client_artifact row
def get_or_create
begin
name = params[:name]
provider = params[:provider]
active = params[:active]
beta = params.has_key?(:beta) ? params[:beta] : true
ip = params[:ip]
username = params[:username]
password = params[:password]
topology = params[:topology]
ars_id = params[:ars_id]
# Explicit field-based lookups
ars = nil
ars = JamRuby::Ars.find_by_id_int(ars_id) if ars_id
ars ||= JamRuby::Ars.find_by_name(name) if name
if ars.nil?
ars = JamRuby::Ars.new
ars.name = name
end
ars.id_int = ars_id if !ars_id.nil?
ars.provider = provider
ars.active = active
ars.beta = params[:beta]
ars.beta = beta
ars.ip = ip
ars.password = password
ars.username = username
if topology
ars.city = topology['city']
ars.country = topology['country']
ars.continent = topology['continent']
ars.latitude = topology['latitude']
ars.longitude = topology['longitude']
ars.subdivision = topology['subdivision']
end
ars.save!
@ars = ars
render :json => {id_int: @ars.id_int, id: @ars.id, name: @ars.name, provider: @ars.provider, active: @ars.active, beta: @ars.beta, ip: @ars.ip}, :status => :ok
rescue => e
render :json => {error: e.message, backtrace: e.backtrace.first(5)}, :status => 500
end
end
end

View File

@ -44,6 +44,8 @@ JamAdmin::Application.routes.draw do
match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv'
match '/api/arses/register' => 'arses#get_or_create', :via => :post
match '/api/arses' => 'arses#index', :via => :get
match '/api/arses/:id' => 'arses#update', :via => :post
mount Resque::Server.new, :at => "/resque"

1
admin/trigger.txt Normal file
View File

@ -0,0 +1 @@
# trigger build

View File

@ -12,3 +12,5 @@ REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
PUBLIC_URL=
REACT_APP_COOKIE_DOMAIN=.jamkazam.local
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE
REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms

View File

@ -8,4 +8,6 @@ REACT_APP_BITBUCKET_BUILD_NUMBER=dev
REACT_APP_BITBUCKET_COMMIT=dev
REACT_APP_ENV=development
REACT_APP_COOKIE_DOMAIN=.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
REACT_APP_GOOGLE_ANALYTICS_ID=G-MC9BTWXWY4
REACT_APP_RECURLY_PUBLIC_API_KEY=
REACT_APP_BRAINTREE_TOKEN=

View File

@ -9,3 +9,5 @@ REACT_APP_RECAPTCHA_ENABLED=true
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_COOKIE_DOMAIN=.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-SPTNJRW7WB
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-hvDV1xQxDw0HPaaRFP4KNE
REACT_APP_BRAINTREE_TOKEN=production_hc7z69yq_pwwc6zm3d478kfrh

View File

@ -9,3 +9,5 @@ REACT_APP_RECAPTCHA_ENABLED=false
REACT_APP_SITE_KEY=6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B
REACT_APP_COOKIE_DOMAIN=.staging.jamkazam.com
REACT_APP_GOOGLE_ANALYTICS_ID=G-8W0GTL53NT
REACT_APP_RECURLY_PUBLIC_API_KEY=ewr1-AjUHUfcLtIsPdtetD4mj2x
REACT_APP_BRAINTREE_TOKEN=sandbox_pgjp8dvs_5v5rwm94m2vrfbms

View File

@ -38,3 +38,19 @@ npm run start
This will open it in a borwser window at http://beta.jamkazam.local:3000. Of course for it to work you also need Rails (web) app and websocket app (websocket-gateway) running.
## Working with JamTracks
if you have the latest from develop, you can go:
```
cd cicd
npm install
./export_personal_jamtracks.sh
./generate.js
open http://beta.jamkazam.local:4000/backing-tracks/ac-dc/back-in-black.html
```
You can also do none of the above, and go straight to:
http://beta.jamkazam.local:4000/public/backing-tracks/ac-dc/back-in-black
I tried to make it so the SPA has 'secret routes' to these pages, which is convenient for us dev & testing
but also tried to make it convenient to run the cicd approach of actually generating separate pages for each landing page (which is what those 5 steps cover)

View File

@ -0,0 +1,29 @@
/// <reference types="cypress" />
import makeFakeUser from '../../factories/user';
describe('Change Email Confirm Page', () => {
beforeEach(() => {
// Log in to the application or navigate to the account page
// where the change email feature is available
const currentUser = makeFakeUser({
email: 'sam@example.com'
});
cy.stubAuthenticate({ ...currentUser });
cy.intercept('POST', /\S+\/update_email/, { statusCode: 200, body: { ok: true } });
});
it('should display the confirm page when visiting the confirm URL', () => {
// Replace with a realistic token for your app if needed
const token = 'dummy-confirm-token';
// Visit the confirm URL
cy.visit(`/confirm-email-change?token=${token}`);
// Assert that the JKChangeEmailConfirm page is rendered
// Adjust selectors/texts as per your actual component
cy.contains('Change Email Confirmation').should('be.visible');
cy.contains('Loading...').should('be.visible');
// Optionally, check for the success message after the email update
cy.wait(1000); // Wait for the email update to complete
cy.contains('Your email has been successfully updated.').should('be.visible');
});
});

View File

@ -1,23 +1,31 @@
/// <reference types="cypress" />
import makeFakeUser from '../../factories/user';
describe('Unsubscribe from email link', () => {
beforeEach(() =>{
cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } });
beforeEach(() => {
// cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } });
const currentUser = makeFakeUser({
email: 'sam@example.com'
});
cy.stubAuthenticate({ ...currentUser });
cy.intercept('POST', /\S+\/unsubscribe\/\S+/, { statusCode: 200, body: { ok: true } });
})
it("redirect to home page if tok is not provided", () => {
cy.visit('/unsubscribe');
cy.location('pathname').should('eq', '/');
cy.location('pathname').should('eq', '/errors/404');
});
it.only("show unsubscribed message", () => {
cy.visit('/unsubscribe?tok=123');
cy.location('search')
.should('equal', '?tok=123')
.then((s) => new URLSearchParams(s))
.invoke('get', 'tok')
.should('equal', '123')
cy.contains("successfully unsubscribed")
it("show unsubscribed message", () => {
cy.visit('/unsubscribe/123');
// cy.location('search')
// .should('equal', '?tok=123')
// .then((s) => new URLSearchParams(s))
// .invoke('get', 'tok')
// .should('equal', '123')
cy.contains("You have successfully unsubscribed from JamKazam emails.").should('be.visible');
cy.contains("Loading...").should('not.exist');
});
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
const [password, setPassword] = useState('');
const [isDisabled, setIsDisabled] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const { capchaToken, recaptchaRef, handleRecaptcha } = useRecaptcha();
const { addToCart } = useJamTrackShopping();
@ -52,25 +52,31 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
toast.error(`Registration failed. Please try again.`);
setIsDisabled(false);
}
};
useEffect(() => {
const addJamTrackToCart = async () => {
try{
try {
await addToCart(jamTrack);
}catch(error){
} catch (error) {
console.error(error);
}
}
console.log('currentUser', currentUser);
console.log('jamTrack', jamTrack);
console.log('jamTrackArtistName', jamTrackArtistName);
if (currentUser) {
if(jamTrack){
if (jamTrack) {
console.log('adding jamtrack to cart');
addJamTrackToCart();
}else if(jamTrackArtistName){
} else if (jamTrackArtistName) {
console.log('redirecting to jamtracks artist landing');
history.push(`/jamtracks?artist=${jamTrackArtistName}`);
}else{
history.push('/public/downloads');
} else {
console.log('redirecting to profile');
history.push('/profile');
}
}
}, [currentUser, jamTrack, jamTrackArtistName]);
@ -130,9 +136,9 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
{process.env.REACT_APP_RECAPTCHA_ENABLED === 'true' && (
<FormGroup>
<ReCAPTCHA
sitekey={process.env.REACT_APP_SITE_KEY}
ref={recaptchaRef}
<ReCAPTCHA
sitekey={process.env.REACT_APP_SITE_KEY}
ref={recaptchaRef}
onChange={handleRecaptcha}
/>
</FormGroup>
@ -140,7 +146,7 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
<FormGroup>
<Button color="primary" block className="mt-3" disabled={isDisabled}>
{t('signup')}
{t('signup')}
</Button>
</FormGroup>

View File

@ -33,7 +33,9 @@ const Registration = () => {
try {
const response = await getJamTrackBySlug({ slug });
const jamTrack = await response.json();
console.log('jamTrack', jamTrack);
setJamTrack(jamTrack);
setJamTrackArtistName(jamTrack.original_artist);
setLoading(false);
} catch (error) {
console.error(error);

View File

@ -1,11 +1,27 @@
import React, { Fragment } from 'react';
import React, { Fragment, useEffect } from 'react';
import { Col, Row } from 'reactstrap';
import { Link } from 'react-router-dom';
import { Link, useHistory, useLocation } from 'react-router-dom';
import LoginForm from '../LoginForm';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../context/UserAuth';
const Login = () => {
const { t } = useTranslation('auth');
const { isAuthenticated, isLoading } = useAuth();
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (!isLoading && isAuthenticated) {
const { from } = location.state || { from: { pathname: '/profile' } };
const finalDestination = from.pathname === '/' ? { pathname: '/profile' } : from;
history.replace(finalDestination);
}
}, [isAuthenticated, isLoading, history, location]);
if (isLoading || isAuthenticated) {
return null; // Or a loading spinner
}
return (
<Fragment>

View File

@ -37,6 +37,7 @@ import JKEditProfile from '../page/JKEditProfile';
import JKEditAccount from '../page/JKEditAccount';
import JKAccountSubscription from '../page/JKAccountSubscription';
import JKPaymentHistory from '../page/JKPaymentHistory';
import JKPaymentMethod from '../page/JKPaymentMethod';
import JKAccountPreferences from '../page/JKAccountPreferences';
import JKAffiliateProgram from '../affiliate/JKAffiliateProgram';
@ -54,6 +55,9 @@ import JKMyJamTracks from '../jamtracks/JKMyJamTracks';
import JKJamTrackShow from '../jamtracks/JKJamTrackShow';
import JKPayPalConfirmation from '../shopping-cart/JKPayPalConfirmation';
import JKUnsubscribe from '../public/JKUnsubscribe';
import JKConfirmEmailChange from '../public/JKConfirmEmailChange';
//import loadable from '@loadable/component';
//const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes'));
@ -276,6 +280,10 @@ function JKDashboardMain() {
}
useScript(`${process.env.REACT_APP_CLIENT_BASE_URL}/client_scripts`, initJKScripts);
useScript('https://js.recurly.com/v4/recurly.js', () => {
console.log('Recurly.js script loaded');
window.recurly.configure(process.env.REACT_APP_RECURLY_PUBLIC_API_KEY);
});
return (
<div className={isFluid || isKanban ? 'container-fluid' : 'container'}>
@ -297,6 +305,7 @@ function JKDashboardMain() {
<PrivateRoute path="/account/identity" component={JKEditAccount} />
<PrivateRoute path="/account/subscription" component={JKAccountSubscription} />
<PrivateRoute path="/account/payments" component={JKPaymentHistory} />
<PrivateRoute path="/account/payment-method" component={JKPaymentMethod} />
<PrivateRoute path="/account/preferences" component={JKAccountPreferences} />
<PrivateRoute path="/affiliate/program" component={JKAffiliateProgram} />
<PrivateRoute path="/affiliate/payee" component={JKAffiliatePayee} />
@ -313,6 +322,8 @@ function JKDashboardMain() {
<PrivateRoute path="/checkout/success" component={JKCheckoutSuccess} />
<PrivateRoute path="/checkout" component={JKCheckout} />
<PrivateRoute path="/applaunch" component={JKAppLaunch} />
<PrivateRoute path="/unsubscribe/:tok" exact component={JKUnsubscribe} />
<PrivateRoute path="/confirm-email-change" exact component={JKConfirmEmailChange} />
{/*Redirect*/}
<Redirect to="/errors/404" />
</Switch>

View File

@ -23,7 +23,7 @@ const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.webp';
const BodyComponent = ({
id = "1",
plan_code = "jamtrack-acdc-backinblack",
slug = "ac-dc-back-in-black",
slug = "acdc-back-in-black",
artist = "AC/DC (defaulted)",
song = "Back in Black (defaulted)",
provided_jam_track = null

View File

@ -23,12 +23,24 @@ const ProfileDropdown = () => {
const handleLogout = async event => {
event.preventDefault();
const cookieDomain = `.${process.env.REACT_APP_ORIGIN}`;
console.log('handleLogout: cookie: ', cookieDomain);
removeCookie('remember_token', {
domain: `.${process.env.REACT_APP_ORIGIN}`
domain: cookieDomain,
path: '/'
});
setCurrentUser(null);
// This will cause the server to remove the cookie TOO (overkill),
// but this code has a side effect because something, somewhere,
// client-side history pushes to /auth/login because of this function.
// ...
await logout();
// But I don't want to count on that side effect (defensive against future changes), so I force a page location hit here
window.location.href = "/auth/login";
// And here's the final bit; force a full page refresh ANYWAY.
// This is the only way we seem to really clear
// 'login state' cleanly at this point
window.location.reload();
};
return (

View File

@ -0,0 +1,480 @@
import React, { useState, useEffect, useRef } from 'react';
import {
Card,
CardBody,
Col,
Button,
Row,
CustomInput,
Label
} from 'reactstrap';
import Flex from '../common/Flex';
import FalconCardHeader from '../common/FalconCardHeader';
import { useTranslation } from 'react-i18next';
import iconPaymentMethodsGrid from '../../assets/img/icons/icon-payment-methods-grid.png';
import iconPaypalFull from '../../assets/img/icons/icon-paypal-full.png';
import { toast } from 'react-toastify';
import { updatePayment } from '../../helpers/rest';
import { useAuth } from '../../context/UserAuth';
import { getBillingInfo, getUserDetail, getCountries } from '../../helpers/rest';
import { useForm, Controller } from 'react-hook-form';
import Select from 'react-select';
import { useResponsive } from '@farfetch/react-context-responsive';
const JKPaymentMethod = () => {
const { t } = useTranslation('account');
const [billingInfo, setBillingInfo] = useState({});
const [hasStoredCreditCard, setHasStoredCreditCard] = useState(false);
const [paymentMethod, setPaymentMethod] = useState('credit-card');
const { currentUser } = useAuth();
const [countries, setCountries] = useState([]);
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
const [submitting, setSubmitting] = useState(false);
const [isCardValid, setIsCardValid] = useState(false);
const { greaterThan } = useResponsive();
const elementsRef = useRef(null);
const formRef = useRef(null);
const recurlyConfigured = useRef(false);
const paypal = useRef(null);
const {
register,
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
defaultValues: {
first_name: '',
last_name: '',
address1: '',
address2: '',
city: '',
state: '',
zip: '',
country: 'US',
}
});
useEffect(() => {
if (currentUser) {
populateUserData();
}
}, [currentUser]);
const populateUserData = async () => {
const options = {
id: currentUser.id
};
try {
const userResp = await getUserDetail(options);
const userData = await userResp.json();
if (userData.has_recurly_account) {
setHasStoredCreditCard(userData['has_stored_credit_card?']);
await populateBillingAddress();
}
} catch (error) {
console.error('Failed to get user details:', error);
}
};
const populateBillingAddress = async () => {
try {
const resp = await getBillingInfo();
const data = await resp.json();
const bi = data.billing_info;
setBillingInfo(bi);
} catch (error) {
console.error('Failed to get billing info:', error);
}
};
useEffect(() => {
if (currentUser) {
fetchCountries();
}
}, [currentUser]);
const fetchCountries = () => {
getCountries()
.then(response => {
if (response.ok) {
return response.json();
}
})
.then(data => {
setCountries(data.countriesx);
})
.catch(error => console.log(error));
};
useEffect(() => {
if (billingInfo) {
setValue('first_name', billingInfo.first_name || '');
setValue('last_name', billingInfo.last_name || '');
setValue('address1', billingInfo.address1 || '');
setValue('address2', billingInfo.address2 || '');
setValue('city', billingInfo.city || '');
setValue('state', billingInfo.state || '');
setValue('zip', billingInfo.zip || '');
setValue('country', billingInfo.country || 'US');
}
}, [billingInfo, setValue]);
const handleCountryChange = selectedOption => {
setValue('country', selectedOption.value);
};
const recurlyContainerRef = useRef();
useEffect(() => {
if (!recurlyContainerRef.current || !window.recurly || recurlyConfigured.current) return;
window.recurly.configure({ publicKey: process.env.REACT_APP_RECURLY_PUBLIC_API_KEY });
const elements = window.recurly.Elements();
const cardElement = elements.CardElement();
cardElement.attach(recurlyContainerRef.current);
cardElement.on('change', (event) => {
setIsCardValid(event.complete && !event.error);
});
//then load paypal:
const paypalInst = window.recurly.PayPal({ braintree: { clientAuthorization: process.env.REACT_APP_BRAINTREE_TOKEN } })
paypal.current = paypalInst;
paypal.current.on('error', onPayPalError);
paypal.current.on('token', onPayPalToken);
elementsRef.current = elements;
recurlyConfigured.current = true;
return () => {
// Optional cleanup if the component unmounts
recurlyContainerRef.current.innerHTML = '';
recurlyConfigured.current = false;
};
}, [recurlyContainerRef.current]);
const onPayPalError = (error) => {
console.error('PayPal Error:', error);
toast.error('PayPal Error: ' + (error.message || t('payment_method.alerts.try_again')));
setSubmitting(false);
}
const onPayPalToken = (token) => {
handleUpdatePayment(token);
}
const handleUpdatePayment = (token) => {
updatePayment({ recurly_token: token.id }).then((response) => {
setHasStoredCreditCard(true);
toast.success(t('payment_method.alerts.payment_method_updated'));
}).catch((error) => {
console.error('Error updating payment with PayPal token:', error);
if (error.response && error.response.data && error.response.data.message) {
toast.error(error.response.data.message);
} else {
console.error('Error updating payment with PayPal token:', error);
toast.error(t('payment_method.alerts.card_update_error'));
}
}).finally(() => {
setSubmitting(false);
});
};
const onSubmit = async (data) => {
setSubmitting(true);
if (paymentMethod === 'paypal') { // PayPal payment method
handoverToPaypal();
return;
} else { // Credit Card payment method
if (!elementsRef.current) {
console.error('Recurly elementsRef.current is not ready');
setSubmitting(false);
return;
}
if (!formRef.current) {
console.error('formRef.current is not ready');
setSubmitting(false);
return;
}
window.recurly.token(elementsRef.current, formRef.current, (err, token) => {
if (err) {
console.error('Recurly token error:', err);
toast.error(t('payment_method.alerts.card_processing_error'));
setSubmitting(false);
} else {
console.log('Recurly token:', token.id);
// send token.id to backend
handleUpdatePayment(token);
}
});
}
};
const handoverToPaypal = () => {
// Handover to Paypal
setSubmitting(true);
paypal.current.start()
};
return (
<Card>
<FalconCardHeader title={t('payment_method.page_title')} titleClass="font-weight-bold" />
<CardBody className="pt-3" style={{ backgroundColor: '#edf2f9' }}>
<div className='mb-3'>
{hasStoredCreditCard ? (
<span>
<strong>{t('payment_method.help_text_has_card')}</strong>
</span>
) : (
<span>
<strong>{t('payment_method.help_text_no_card')}</strong>
</span>
)}
&nbsp;{t('payment_method.help_text')}
</div>
<form onSubmit={handleSubmit(onSubmit)} ref={formRef}>
<Card style={{ width: greaterThan.sm ? "90%" : '100%' }} className='mx-auto'>
<FalconCardHeader title={t('payment_method.header')} titleTag="h5" />
<CardBody>
<Row>
<Col className="mb-2" xs={12} md={6}>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="first_name" className={labelClassName}>
{t('payment_method.first_name')}
</Label>
</Col>
<Col>
<input {...register('first_name', { required: t('payment_method.validations.first_name.required') })} className="form-control" data-recurly="first_name" />
{errors.first_name && (
<div className="text-danger">
<small>{errors.first_name.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="last_name" className={labelClassName}>
{t('payment_method.last_name')}
</Label>
</Col>
<Col>
<input {...register('last_name', { required: t('payment_method.validations.last_name.required') })} className="form-control" data-recurly="last_name" />
{errors.last_name && (
<div className="text-danger">
<small>{errors.last_name.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="address1" className={labelClassName}>
{t('payment_method.address1')}
</Label>
</Col>
<Col>
<input {...register('address1', { required: t('payment_method.validations.address1.required') })} className="form-control" data-recurly="address1" />
{errors.address1 && (
<div className="text-danger">
<small>{errors.address1.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="address2" className={labelClassName}>
{t('payment_method.address2')}
</Label>
</Col>
<Col>
<input {...register('address2')} className="form-control" data-recurly="address2" />
{errors.address2 && (
<div className="text-danger">
<small>{errors.address2.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="city" className={labelClassName}>
{t('payment_method.city')}
</Label>
</Col>
<Col>
<input {...register('city', { required: t('payment_method.validations.city.required') })} className="form-control" data-recurly="city" />
{errors.city && (
<div className="text-danger">
<small>{errors.city.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="state" className={labelClassName}>
{t('payment_method.state')}
</Label>
</Col>
<Col>
<input {...register('state', { required: t('payment_method.validations.state.required') })} className="form-control" data-recurly="state" />
{errors.state && (
<div className="text-danger">
<small>{errors.state.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="zip" className={labelClassName}>
{t('payment_method.zip_code')}
</Label>
</Col>
<Col>
<input
{...register('zip', { required: t('payment_method.validations.zip_code.required') })}
className="form-control" data-recurly="postal_code"
/>
{errors.zip && (
<div className="text-danger">
<small>{errors.zip.message}</small>
</div>
)}
</Col>
</Row>
<Row className="mb-2">
<Col xs={12} md={5} lg={4} className="text-md-right">
<Label for="country" className={labelClassName}>
{t('payment_method.country')}
</Label>
</Col>
<Col>
<Controller
name="country"
control={control}
rules={{ required: t('payment_method.validations.country.required') }}
render={({ field: { onChange, value } }) => {
const country = countries.find(country => country.countrycode === value);
if (!country) {
return (
<Select
data-testid="countrySelect"
data-recurly="country"
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}
return (
<Select
data-testid="countrySelect"
data-recurly="country"
value={{ value: country.countrycode, label: country.countryname }}
onChange={handleCountryChange}
options={countries.map(c => {
return { value: c.countrycode, label: c.countryname };
})}
/>
);
}}
/>
<input type="hidden" name="country" data-recurly="country" {...register('country')} />
{errors.country && (
<div className="text-danger">
<small>{errors.country.message}</small>
</div>
)}
</Col>
</Row>
</Col>
<Col xs={12} md={6} className="mb-2 pl-5">
<Row>
<Col xs={12}>
<CustomInput
label={
<Flex align="center" className="mb-2 fs-1">
<span>{t('payment_method.credit_card')}</span>
</Flex>
}
id="credit-card"
value="credit-card"
checked={paymentMethod === 'credit-card'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<Row>
<Col sm={8}>
<div id="recurly-elements" ref={recurlyContainerRef}></div>
{!isCardValid && errors.recurly && (
<div className="text-danger">
<small>{errors.recurly.message}</small>
</div>
)}
<input type="hidden" name="recurly-token" data-recurly="token" />
</Col>
<div className="col-4 text-center pt-2 d-none d-sm-block">
<div className="rounded p-2 mt-3 bg-100">
<div className="text-uppercase fs--2 font-weight-bold">{t('payment_method.we_accept')}</div>
<img src={iconPaymentMethodsGrid} alt="" width="120" />
</div>
</div>
</Row>
<hr />
<Row className="mt-3">
<Col xs={12}>
<CustomInput
label={<img className="pull-right" src={iconPaypalFull} height="20" alt="" />}
id="paypal"
value="paypal"
checked={paymentMethod === 'paypal'}
onChange={({ target }) => setPaymentMethod(target.value)}
type="radio"
/>
</Col>
</Row>
<div className="d-flex justify-content-center">
<Button
color="primary"
type="submit"
disabled={submitting}
className="mt-3"
>
{submitting ? t('payment_method.submitting') : t('payment_method.save_payment_info')}
</Button>
</div>
<div className="text-center">
<p className="fs--1 mt-3 mb-0">
{t('payment_method.aggreement.text1')} <strong>{t('payment_method.aggreement.text2')} </strong>{t('payment_method.aggreement.text3')}{' '}
<br />
<a href="https://www.jamkazam.com/corp/terms" target='_blank'>{t('payment_method.aggreement.terms_of_service')}</a>
</p>
</div>
</Col>
</Row>
</CardBody>
</Card>
</form>
</CardBody>
</Card>
);
};
export default JKPaymentMethod;

View File

@ -0,0 +1,55 @@
import React from 'react'
import { useLocation } from "react-router-dom";
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useState, useEffect } from 'react';
import { updateEmail } from '../../helpers/rest';
import { useTranslation } from 'react-i18next';
const JKConfirmEmailChange = () => {
const { t } = useTranslation("account");
const location = useLocation();
const params = new URLSearchParams(location.search);
const token = params.get('token');
const [success, setSuccess] = useState(false);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (token) {
updateEmail(token)
.then(response => {
if (response.status === 200) {
setSuccess(true);
} else {
setSuccess(false);
}
})
.catch(() => {
setSuccess(false);
setError(t('identity.changed_email_confirmation.error'));
}).finally(() => {
setLoading(false);
});
}
}, [token]);
return (
<Card style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">
{t('identity.changed_email_confirmation.title')}
</CardTitle>
<>
{loading && <div className="text-muted"><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>{t('identity.changed_email_confirmation.loading')}</div>}
{success && <div className="text-success">{t('identity.changed_email_confirmation.success')}</div>}
{error && <div className="text-danger">{error}</div>}
</>
</CardBody>
</Card>
)
}
export default JKConfirmEmailChange

View File

@ -1,16 +1,18 @@
import React, { useEffect, useState } from 'react';
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useTranslation } from "react-i18next";
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useTranslation } from 'react-i18next';
import { useBrowserQuery } from '../../context/BrowserQuery';
import { useHistory } from "react-router-dom";
import { useHistory, useParams } from "react-router-dom";
const unsubscribeFromNewUsersWeeklyEmail = (token) => {
const baseUrl = process.env.REACT_APP_LEGACY_BASE_URL
const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL
return new Promise((resolve, reject) => {
fetch(`${baseUrl}/unsubscribe_user_match/${token}`,
fetch(`${baseUrl}/unsubscribe_user_match/${token}`,
{ method: 'POST' }
).then(response => {
if (response.ok) {
@ -22,43 +24,64 @@ const unsubscribeFromNewUsersWeeklyEmail = (token) => {
})
}
const unsubscribe = (token) => {
const baseUrl = process.env.REACT_APP_CLIENT_BASE_URL
return new Promise((resolve, reject) => {
fetch(`${baseUrl}/unsubscribe/${token}`, { method: 'POST', headers: { 'Content-Type': 'application/json', accept: 'application/json' } })
.then(response => {
if (response.ok) {
resolve(response)
} else {
reject(response)
}
}).catch(error => {
reject(error);
});
})
}
function JKUnsubscribe() {
const {t} = useTranslation()
const queryObj = useBrowserQuery();
const { t } = useTranslation("unsubscribe");
const history = useHistory()
const [loading, setLoading] = useState(true)
const [success, setSuccess] = useState(false)
const [error, setError] = useState(null)
const { tok } = useParams();
useEffect(() => {
const token = queryObj.get('tok')
if(token){
unsubscribeFromNewUsersWeeklyEmail(token)
.then((resp) => {
if(resp.ok){
setSuccess(true)
}
})
.catch(error => console.error(error))
}else{
if (tok) {
unsubscribe(tok)
.then((resp) => {
if (resp.ok) {
setSuccess(true)
} else {
setSuccess(false)
}
})
.catch(error => {
setError(error)
}).finally(() => {
setLoading(false)
});
} else {
history.push('/')
}
}, [])
return (
<Card color={ success ? 'success' : 'light' } style={{ width: '25rem', margin: '2rem auto' }}>
<Card style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">Unsubscribe From Weekly Email</CardTitle>
<CardText>
{
success?
'You have successfully unsubscribed from weekly emails on newly joined musicians having low internet latency to you.' :
'Unsubscribing...'
}
</CardText>
<CardTitle className="mb-2">{t('page_title')}</CardTitle>
<>
{loading && <div className="text-muted"><span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>{t('loading')}</div>}
{success && <div className="text-success">{t('success')}</div>}
{error && <div className="text-danger">{t('error')}</div>}
</>
</CardBody>
</Card>
)
}

View File

@ -0,0 +1,99 @@
/**
* meta_tracking.js
* A standalone module to capture and persist Meta attribution signals (fbclid, _fbp) in cookies.
*
* Logic adapted from web/app/assets/javascripts/meta_tracking.js for React environment.
* - Checks URL for `fbclid` and sets `_fbc` cookie.
* - Checks for `_fbp` cookie; if missing, generates and sets it.
*/
const MetaTracking = {
init: function () {
const location = window.location;
this.handleFbc(location.search);
this.handleFbp();
this.handleUtm(location.search);
},
// 1. Parsing and storing _fbc (Click ID)
handleFbc: function (searchParams) {
const fbclid = this.getQueryParam('fbclid', searchParams);
if (fbclid) {
const version = 'fb';
const subdomainIndex = 1; // 1 = example.com
const creationTime = new Date().getTime(); // Unix timestamp in ms
// Format: fb.1.timestamp.id
const fbcValue = `${version}.${subdomainIndex}.${creationTime}.${fbclid}`;
this.setCookie('_fbc', fbcValue, 90);
}
},
handleUtm: function (searchParams) {
if (!searchParams) return;
const query = searchParams.substring(1);
const vars = query.split('&');
vars.forEach(v => {
const pair = v.split('=');
if (pair.length === 2) {
const key = decodeURIComponent(pair[0]);
const value = decodeURIComponent(pair[1]);
if (key.indexOf('utm_') === 0) {
this.setCookie(key, value, 90);
}
}
});
},
// 2. Handling _fbp (Browser ID)
handleFbp: function () {
if (!this.getCookie('_fbp')) {
const version = 'fb';
const subdomainIndex = 1;
const creationTime = new Date().getTime();
const randomInt = Math.floor(Math.random() * 10000000000); // 10-digit random number
// Format: fb.1.timestamp.randomDigits
const fbpValue = `${version}.${subdomainIndex}.${creationTime}.${randomInt}`;
this.setCookie('_fbp', fbpValue, 90);
}
},
// Helper: Get query param by name
getQueryParam: function (name, search) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
},
// Helper: Set cookie
setCookie: function (name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
// Ensure path is root and domain is included if needed (defaults to current host)
document.cookie = name + "=" + (value || "") + expires + "; path=/";
},
// Helper: Get cookie
getCookie: function (name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
};
export default MetaTracking;

View File

@ -101,8 +101,8 @@ export const getInstruments = () => {
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
apiFetch('/me')
.then(response => resolve(response))
.catch(error => reject(error))
.then(response => resolve(response))
.catch(error => reject(error))
})
}
@ -289,7 +289,7 @@ export const getCities = (countryId, regionId) => {
export const postUpdateAccountEmail = (userId, options) => {
const { email, current_password } = options;
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/update_email`, {
apiFetch(`/users/${userId}/update_email_alt`, {
method: 'POST',
body: JSON.stringify({ update_email: email, current_password })
})
@ -339,7 +339,7 @@ export const requstResetForgotPassword = email => {
})
.then(response => resolve(response))
.catch(error => reject(error));
});
});
};
export const resetForgotPassword = (options = {}) => {
@ -491,18 +491,16 @@ export const getJamTrackPublic = options => {
const { plan_code } = options;
return new Promise((resolve, reject) => {
// This does not make sense; historical reasons here
apiFetch(`/jamtracks/band/${plan_code}?${new URLSearchParams({plan_code})}`)
.then(response => resolve(response))
.catch(error => reject(error));
apiFetch(`/jamtracks/band/${plan_code}?${new URLSearchParams({ plan_code })}`)
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const getJamTrackBySlug = options => {
const { slug } = options;
return new Promise((resolve, reject) => {
apiFetch(`/jamtracks/${slug}`, {
baseUrl: process.env.REACT_APP_CLIENT_BASE_URL
})
apiFetch(`/jamtracks/public/${slug}`)
.then(response => resolve(response))
.catch(error => reject(error));
});
@ -554,6 +552,18 @@ export const getBillingInfo = () => {
});
};
export const updateBillingInfo = (options = {}) => {
const params = { billing_info: options };
return new Promise((resolve, reject) => {
apiFetch(`/recurly/update_billing_info`, {
method: 'PUT',
body: JSON.stringify(params)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
export const createRecurlyAccount = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/recurly/create_account`, {
@ -671,7 +681,7 @@ export const createAlert = (subject, data) => {
return new Promise((resolve, reject) => {
apiFetch(`/alerts`, {
method: 'POST',
body: JSON.stringify({subject, data})
body: JSON.stringify({ subject, data })
})
.then(response => resolve(response))
.catch(error => reject(error));
@ -688,8 +698,8 @@ export const getClientDownloads = () => {
export const getObsPluginDownloads = () => {
return new Promise((resolve, reject) => {
apiFetch(`/artifacts/OBSPlugin`)
.then(response => resolve(response))
.catch(error => reject(error));
.then(response => resolve(response))
.catch(error => reject(error));
});
}
@ -704,3 +714,61 @@ export const paypalPlaceOrder = (options = {}) => {
.catch(error => reject(error));
});
};
export const submitStripe = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/stripe`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
})
}
// function updatePayment(options) {
// options = options || {}
// return $.ajax({
// type: "POST",
// url: '/api/recurly/update_payment',
// dataType: "json",
// contentType: 'application/json',
// data: JSON.stringify(options)
// })
// }
export const updatePayment = (options = {}) => {
return new Promise((resolve, reject) => {
apiFetch(`/recurly/update_payment`, {
method: 'POST',
body: JSON.stringify(options)
})
.then(response => resolve(response))
.catch(error => reject(error));
});
};
// function postUpdateEmail(email, current_password) {
// var url = "/api/users/" + context.JK.currentUserId + "/update_email";
// return $.ajax({
// type: "POST",
// dataType: "json",
// contentType: 'application/json',
// url: url,
// data: JSON.stringify({ update_email: email, current_password: current_password }),
// processData: false
// });
// }
export const updateEmail = (userId, email, current_password) => {
return new Promise((resolve, reject) => {
apiFetch(`/users/${userId}/update_email`, {
method: 'POST',
body: JSON.stringify({ update_email: email, current_password })
})
.then(response => resolve(response))
.catch(error => reject(error));
});
}

View File

@ -26,6 +26,12 @@
"confirmation_email_sent": "A confirmation email has been sent to your email address. Please click the link in the email to confirm your email address."
}
},
"changed_email_confirmation": {
"title": "Change Email Confirmation",
"loadding": "Loading...",
"success": "Your email has been successfully changed.",
"error": "An error occurred while confirming your email change. Please try again later."
},
"password_form": {
"title": "Password",
"help_text": "To update the password associated with your account, enter your current password (for security reasons) and the new password, and click the \"Save Password\" button.",
@ -109,5 +115,68 @@
"no_payments": "No payments found.",
"load_more": "Load More",
"loading": "Loading..."
},
"payment_method": {
"page_title": "Payment Method",
"header": "Address and Payment Method",
"help_text_no_card": "You do not currently have a payment method on file.",
"help_text_has_card": "You currently have a payment method on file.",
"help_text": "To update your payment method, first enter your billing address and click the save button. If credit card, enter your card information and click the Save button. If PayPal, click the save button and follow PayPal's on-screen instructions to sign into your account and authorize payment to JamKazam.",
"credit_card_number": "Credit Card Number",
"expiration_date": "Expiration Date (MM/YY)",
"cvv": "CVV",
"submit": "Save Payment Method",
"first_name": "First Name",
"last_name": "Last Name",
"address1": "Address 1",
"address2": "Address 2",
"city": "City",
"state": "State or Region",
"zip_code": "Zip/Postal Code",
"country": "Country",
"credit_card": "Credit Card",
"paypal": "PayPal",
"we_accept": "We accept",
"submitting": "Submitting...",
"save_payment_info": "Save Payment Information",
"validations": {
"first_name": {
"required": "First Name is required"
},
"last_name": {
"required": "Last Name is required"
},
"address1": {
"required": "Address Line 1 is required"
},
"city": {
"required": "City is required"
},
"state": {
"required": "State or Region is required"
},
"zip_code": {
"required": "Zip/Postal Code is required"
},
"country": {
"required": "Country is required"
},
"card": {
"invalid": "Credit card details are invalid"
}
},
"aggreement": {
"text1": "By clicking",
"text2": "Save Payment Information",
"text3": "you agree to JamKazam's",
"terms_of_service": "Terms of Service"
},
"alerts": {
"try_again": "Please try again.",
"payment_method_updated": "Your payment method has been successfully updated.",
"card_update_error": "Failed to update payment method. Please try again later.",
"card_processing_error": "There was an error processing your card. Please check your details and try again.",
"billing_update_error": "There was an error processing your billing information. Please check your details and try again."
}
}
}

View File

@ -1,3 +1,6 @@
{
"page_title": "Unsubscribe"
"page_title": "Unsubscribe from JamKazam emails",
"success": "You have successfully unsubscribed from JamKazam emails.",
"error": "An error occurred while unsubscribing. Please try again later.",
"loading": "Loading..."
}

View File

@ -1,4 +1,4 @@
import React, {useEffect} from 'react';
import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify';
import { CloseButton, Fade } from '../components/common/Toast';
@ -8,6 +8,7 @@ import ErrorLayout from './ErrorLayout';
import BuildMeta from "./JKBuildMeta";
import loadable from '@loadable/component';
import MetaTracking from "../helpers/MetaTracking";
const AuthBasicLayout = loadable(() => import('./JKAuthBasicLayout'));
const PublicLayout = loadable(() => import('./JKPublicLayout'));
@ -15,6 +16,7 @@ const Layout = () => {
useEffect(() => {
AuthBasicLayout.preload();
PublicLayout.preload();
MetaTracking.init();
//see if there is affiliate in query string and save it as cookie
const urlParams = new URLSearchParams(window.location.search);
@ -28,7 +30,7 @@ const Layout = () => {
}, []);
return (
<>
<>
<Switch>
<Route path="/errors" component={ErrorLayout} />
<Route path="/auth" component={AuthBasicLayout} />
@ -37,7 +39,7 @@ const Layout = () => {
</Switch>
<ToastContainer transition={Fade} closeButton={<CloseButton />} position={toast.POSITION.BOTTOM_RIGHT} />
<BuildMeta />
</>
</>
);
};

View File

@ -11,6 +11,7 @@ import JKUnsubscribe from '../components/public/JKUnsubscribe';
import JKDownloads from '../components/public/JKDownloads';
import JKDownloadsLegacy from '../components/public/JKDownloadsLegacy';
import JKObsDownloads from '../components/public/JKObsDownloads';
import JKConfirmEmailChange from '../components/public/JKConfirmEmailChange';
import JKJamTracksLanding from '../components/jamtracks/JKJamTracksLandingDev';
import JKJamTracksArtistLanding from '../components/jamtracks/JKJamTracksArtistLandingDev';
@ -22,7 +23,8 @@ const JKPublicRoutes = ({ match: { url } }) => (
<Route path={`${url}/knowledge-base`} component={JKKnowledgeBase} />
<Route path={`${url}/help-desk`} component={JKHelpDesk} />
<Route path={`${url}/forum`} component={JKForum} />
<Route path={`${url}/unsubscribe`} exact component={JKUnsubscribe} />
{/* <Route path={`${url}/unsubscribe/:tok`} exact component={JKUnsubscribe} />
<Route path={`${url}/confirm-email-change`} exact component={JKConfirmEmailChange} /> */}
<Route path={`${url}/downloads`} exact component={JKDownloads} />
<Route path={`${url}/downloads-legacy`} exact component={JKDownloadsLegacy} />
<Route path={`${url}/obs-plugin-download`} exact component={JKObsDownloads} />

View File

@ -19,11 +19,5 @@
execute "ALTER TABLE users DROP COLUMN profile_complete_reminder3_sent_at"
end
end
=begin
ALTER TABLE users ADD COLUMN profile_completed_at TIMESTAMP;
CREATE INDEX index_users_on_profile_completed_at ON users USING btree (profile_completed_at);
ALTER TABLE users ADD COLUMN profile_complete_reminder1_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN profile_complete_reminder2_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN profile_complete_reminder3_sent_at TIMESTAMP;
UPDATE users set profile_completed_at=NOW() WHERE users.id IN (SELECT player_id FROM musicians_instruments) OR users.id IN (SELECT player_id FROM genre_players);
end

View File

@ -6,7 +6,7 @@
# affiliate_quarterly_payments.subscription_due_amount_in_cents
# affiliate_monthly_payments.jamtrack_due_amount_in_cents
# affiliate_monthly_payments.subscription_due_amount_in_cents
class AddTrackingTotalsToAffiliatePartners < ActiveRecord::Migration
class AffiliateTrackingTotals < ActiveRecord::Migration
def self.up
execute "ALTER TABLE affiliate_partners ADD COLUMN jamtrack_cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0"
@ -16,16 +16,16 @@ class AddTrackingTotalsToAffiliatePartners < ActiveRecord::Migration
execute "ALTER TABLE affiliate_partners ADD COLUMN subscriptions_current_quarter_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN jamtracks_sold INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_partners ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_monthy_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_monthly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
execute "ALTER TABLE affiliate_monthly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_monthy_payments ADD COLUMN subscriptions_count INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
# execute "ALTER TABLE affiliate_quarterly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
#execute "ALTER TABLE affiliate_monthly_payments ADD COLUMN jamtrack_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
#execute "ALTER TABLE affiliate_monthly_payments ADD COLUMN subscription_due_amount_in_cents INTEGER NOT NULL DEFAULT 0"
execute "CREATE INDEX affiliate_partner_user_id_idx ON affiliate_partners USING btree (partner_user_id);"
execute "CREATE INDEX affiliate_quarterly_payments_closed_index ON affiliate_quarterly_payments USING btree (paid);"
execute "CREATE INDEX affiliate_quarterly_payments_paid_index ON affiliate_quarterly_payments USING btree (closed);"
execute "CREATE INDEX affiliate_monthly_payments_paid_index ON affiliate_monthly_payments USING btree (closed);"
# execute "CREATE INDEX affiliate_quarterly_payments_closed_index ON affiliate_quarterly_payments USING btree (paid);"
# execute "CREATE INDEX affiliate_quarterly_payments_paid_index ON affiliate_quarterly_payments USING btree (closed);"
# execute "CREATE INDEX affiliate_monthly_payments_paid_index ON affiliate_monthly_payments USING btree (closed);"
end
=begin
@ -54,7 +54,7 @@ class AddTrackingTotalsToAffiliatePartners < ActiveRecord::Migration
execute "ALTER TABLE affiliate_partners DROP COLUMN jamtrack_cumulative_earnings_in_cents"
execute "ALTER TABLE affiliate_partners DROP COLUMN subscriptions_cumulative_earnings_in_cents"
execute "ALTER TABLE affiliate_partners DROP COLUMN subscriptions_count"
execute "ALTER TABLE affiliate_quarterly_payments DROP COLUMN jamtrack_due_amount_in_cents"
execute "ALTER TABLE affiliate_quarterly_payments DROP COLUMN subscription_due_amount_in_cents"
# execute "ALTER TABLE affiliate_quarterly_payments DROP COLUMN jamtrack_due_amount_in_cents"
# execute "ALTER TABLE affiliate_quarterly_payments DROP COLUMN subscription_due_amount_in_cents"
end
end

View File

@ -11,10 +11,3 @@
execute "ALTER TABLE users DROP COLUMN gear_setup_reminder3_sent_at"
end
end
=begin
ALTER TABLE users ADD COLUMN gear_setup_reminder1_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN gear_setup_reminder2_sent_at TIMESTAMP;
ALTER TABLE users ADD COLUMN gear_setup_reminder3_sent_at TIMESTAMP;
end

View File

@ -0,0 +1,12 @@
class AddGroupSessionReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN group_session_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN group_session_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN group_session_reminder3_sent_at TIMESTAMP"
end
def self.down
execute "ALTER TABLE users DROP COLUMN group_session_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN group_session_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN group_session_reminder3_sent_at"
end
end

View File

@ -0,0 +1,19 @@
class AddTrailExpiresReminderColumnsToUsers < ActiveRecord::Migration
def self.up
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder3_sent_at TIMESTAMP"
# slide in some more production indexes
execute "CREATE INDEX index_users_on_first_music_session_at ON users USING btree (first_music_session_at)"
# subscription_sync_code
execute "CREATE INDEX index_users_on_subscription_sync_code ON users USING btree (subscription_sync_code)"
# first_certified_gear_at
execute "CREATE INDEX index_users_on_first_certified_gear_at ON users USING btree (first_certified_gear_at)"
end
def self.down
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder2_sent_at"
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder3_sent_at"
end
end

View File

@ -0,0 +1,11 @@
class AddFacebookTrackingToUsers < ActiveRecord::Migration
def up
execute "ALTER TABLE users ADD COLUMN facebook_click_id varchar(1000)"
execute "ALTER TABLE users ADD COLUMN facebook_browser_id varchar(1000)"
end
def down
execute "ALTER TABLE users DROP COLUMN facebook_click_id"
execute "ALTER TABLE users DROP COLUMN facebook_browser_id"
end
end

View File

@ -0,0 +1,31 @@
class AddExtendedUtmToUsers < ActiveRecord::Migration[5.0]
def up
execute <<-SQL
ALTER TABLE users ADD COLUMN origin_id character varying;
ALTER TABLE users ADD COLUMN origin_term character varying;
ALTER TABLE users ADD COLUMN origin_content character varying;
CREATE INDEX index_users_on_origin_id ON users (origin_id);
CREATE INDEX index_users_on_origin_term ON users (origin_term);
CREATE INDEX index_users_on_origin_content ON users (origin_content);
CREATE INDEX index_users_on_origin_utm_source ON users (origin_utm_source);
CREATE INDEX index_users_on_origin_utm_medium ON users (origin_utm_medium);
SQL
end
def down
execute <<-SQL
DROP INDEX IF EXISTS index_users_on_origin_utm_medium;
DROP INDEX IF EXISTS index_users_on_origin_utm_source;
DROP INDEX IF EXISTS index_users_on_origin_content;
DROP INDEX IF EXISTS index_users_on_origin_term;
DROP INDEX IF EXISTS index_users_on_origin_id;
ALTER TABLE users DROP COLUMN IF EXISTS origin_content;
ALTER TABLE users DROP COLUMN IF EXISTS origin_term;
ALTER TABLE users DROP COLUMN IF EXISTS origin_id;
SQL
end
end

View File

@ -56,6 +56,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/lib/capi_transmitter"
require "jam_ruby/subscription_definitions"
require "jam_ruby/resque/resque_jam_error"
require "jam_ruby/resque/resque_hooks"
@ -123,6 +124,9 @@ require "jam_ruby/lib/musician_filter"
require "jam_ruby/lib/email_profile_reminder"
require "jam_ruby/lib/email_signup_survey"
require "jam_ruby/lib/gear_setup_reminder"
require "jam_ruby/lib/test_gear_reminder"
require "jam_ruby/lib/group_session_reminder"
require "jam_ruby/lib/trial_expires_reminder"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"

View File

@ -436,7 +436,7 @@ module JamRuby
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder2"
mail(:to => user.email, :subject => "Take 2 minutes to fill out your JamKazam profile now") do |format|
mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder2.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
@ -448,7 +448,7 @@ module JamRuby
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder3"
mail(:to => user.email, :subject => "Last reminder to update your JamKazam profile") do |format|
mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder3.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
@ -504,6 +504,54 @@ module JamRuby
end
end
def group_session_reminder1(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.group_session_reminder1.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def group_session_reminder2(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.group_session_reminder2.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def group_session_reminder3(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.group_session_reminder3.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def trial_expires_reminder1(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.trial_expires_reminder1.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def trial_expires_reminder2(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.trial_expires_reminder2.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def trial_expires_reminder3(user)
@user = user
mail(:to => user.email, :subject => I18n.t('user_mailer.trial_expires_reminder3.subject')) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
def signup_survey(user)
@user = user
@subject = I18n.t('user_mailer.signup_survey.subject')

View File

@ -0,0 +1,44 @@
<p><%=I18n.t('user_mailer.group_session_reminder1.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph1').html_safe -%>
</p>
<p>
<h2><%=I18n.t('user_mailer.group_session_reminder1.office_hours') -%></h2>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph2').html_safe -%>
</p>
<p>
<h2><%=I18n.t('user_mailer.group_session_reminder1.hosted_sessions') -%></h2>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph3').html_safe -%>
</p>
<p>
<h2><%=I18n.t('user_mailer.group_session_reminder1.inviting_your_friends') -%></h2>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph4').html_safe -%>
</p>
<p>
<h2><%=I18n.t('user_mailer.group_session_reminder1.finding_good_matches') -%></h2>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph5').html_safe -%>
</p>
<p>
<h2><%=I18n.t('user_mailer.group_session_reminder1.scheduling_sessions') -%></h2>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph6').html_safe -%>
</p>
<p>
<h2><%=I18n.t('user_mailer.group_session_reminder1.asking_for_help') -%></h2>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph7').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph8').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder1.regards') -%>,<br />
<%=I18n.t('user_mailer.group_session_reminder1.signature') -%>
</p>

View File

@ -0,0 +1,26 @@
<%=I18n.t('user_mailer.group_session_reminder1.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.group_session_reminder1.paragraph1') -%>
<%=I18n.t('user_mailer.group_session_reminder1.office_hours') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph2') -%>
<%=I18n.t('user_mailer.group_session_reminder1.hosted_sessions') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph3') -%>
<%=I18n.t('user_mailer.group_session_reminder1.inviting_your_friends') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph4') -%>
<%=I18n.t('user_mailer.group_session_reminder1.finding_good_matches') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph5') -%>
<%=I18n.t('user_mailer.group_session_reminder1.scheduling_sessions') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph6') -%>
<%=I18n.t('user_mailer.group_session_reminder1.asking_for_help') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph7') -%>
<%=I18n.t('user_mailer.group_session_reminder1.paragraph8') -%>
<%=I18n.t('user_mailer.group_session_reminder1.regards') -%>,
<%=I18n.t('user_mailer.group_session_reminder1.signature') -%>

View File

@ -0,0 +1,18 @@
<p><%=I18n.t('user_mailer.group_session_reminder2.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder2.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder2.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder2.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.group_session_reminder2.regards') -%>,<br />
<%=I18n.t('user_mailer.group_session_reminder2.signature') -%>
</p>

View File

@ -0,0 +1,10 @@
<%=I18n.t('user_mailer.group_session_reminder2.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.group_session_reminder2.paragraph1') -%>
<%=I18n.t('user_mailer.group_session_reminder2.paragraph2') -%>
<%=I18n.t('user_mailer.group_session_reminder2.paragraph3') -%>
<%=I18n.t('user_mailer.group_session_reminder2.regards') -%>,
<%=I18n.t('user_mailer.group_session_reminder2.signature') -%>

View File

@ -0,0 +1,10 @@
<%= I18n.t('user_mailer.group_session_reminder3.greeting') -%> <%= @user.first_name -%> -
<%= I18n.t('user_mailer.group_session_reminder3.paragraph1') -%>
<%= I18n.t('user_mailer.group_session_reminder3.paragraph2') -%>
<%= I18n.t('user_mailer.group_session_reminder3.paragraph3') -%>
<%= I18n.t('user_mailer.group_session_reminder3.regards') -%>,
<%= I18n.t('user_mailer.group_session_reminder3.signature') -%>

View File

@ -0,0 +1,10 @@
<%=I18n.t('user_mailer.group_session_reminder3.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.group_session_reminder3.paragraph1') -%>
<%=I18n.t('user_mailer.group_session_reminder3.paragraph2') -%>
<%=I18n.t('user_mailer.group_session_reminder3.paragraph3') -%>
<%=I18n.t('user_mailer.group_session_reminder3.regards') -%>,
<%=I18n.t('user_mailer.group_session_reminder3.signature') -%>

View File

@ -0,0 +1,14 @@
<p><%=I18n.t('user_mailer.trial_expires_reminder1.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder1.regards') -%>,<br />
<%=I18n.t('user_mailer.trial_expires_reminder1.signature') -%>
</p>

View File

@ -0,0 +1,8 @@
<%=I18n.t('user_mailer.trial_expires_reminder1.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph1') -%>
<%=I18n.t('user_mailer.trial_expires_reminder1.paragraph2') -%>
<%=I18n.t('user_mailer.trial_expires_reminder1.regards') -%>,
<%=I18n.t('user_mailer.trial_expires_reminder1.signature') -%>

View File

@ -0,0 +1,18 @@
<p><%=I18n.t('user_mailer.trial_expires_reminder2.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder2.regards') -%>,<br />
<%=I18n.t('user_mailer.trial_expires_reminder2.signature') -%>
</p>

View File

@ -0,0 +1,10 @@
<%=I18n.t('user_mailer.trial_expires_reminder2.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph1') -%>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph2') -%>
<%=I18n.t('user_mailer.trial_expires_reminder2.paragraph3') -%>
<%=I18n.t('user_mailer.trial_expires_reminder2.regards') -%>,
<%=I18n.t('user_mailer.trial_expires_reminder2.signature') -%>

View File

@ -0,0 +1,18 @@
<p><%=I18n.t('user_mailer.trial_expires_reminder3.greeting') -%> <%= @user.first_name -%> -</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph1').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph2').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph3').html_safe -%>
</p>
<p>
<%=I18n.t('user_mailer.trial_expires_reminder3.regards') -%>,<br />
<%=I18n.t('user_mailer.trial_expires_reminder3.signature') -%>
</p>

View File

@ -0,0 +1,10 @@
<%=I18n.t('user_mailer.trial_expires_reminder3.greeting') -%> <%= @user.first_name -%> -
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph1') -%>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph2') -%>
<%=I18n.t('user_mailer.trial_expires_reminder3.paragraph3') -%>
<%=I18n.t('user_mailer.trial_expires_reminder3.regards') -%>,
<%=I18n.t('user_mailer.trial_expires_reminder3.signature') -%>

View File

@ -30,7 +30,7 @@
<p>
<%= I18n.t "mailer_layout.footer.paragraph1" -%> <a href="https://www.jamkazam.com" target="_blank">JamKazam</a>. <br /> <%= I18n.t "mailer_layout.footer.you_can" -%> <a
href="https://www.jamkazam.com/unsubscribe/<%= @user.unsubscribe_token %>"
href="<%= ApplicationHelper.spa_base_uri %>/unsubscribe/<%= @user.unsubscribe_token %>"
style="
color: #2c7be5;
text-decoration: none;

View File

@ -0,0 +1,86 @@
require 'net/http'
require 'uri'
require 'json'
class CapiTransmitter
def self.send_event(event_name, user, custom_data = {}, event_source_url = nil, test_event_code = nil)
begin
puts("CapiTransmitter: Sending event #{event_name} for User #{user.id}")
pixel_id = APP_CONFIG.facebook_pixel_id
access_token = APP_CONFIG.facebook_access_token
if pixel_id.blank? || access_token.blank?
puts("CapiTransmitter: Missing Facebook Pixel ID or Access Token. Skipping event #{event_name}.")
return
end
# Construct the User Data object
user_data = {
# client_ip_address: user.current_sign_in_ip,
# client_user_agent: user.user_agent # Note: User model might not have user_agent stored directly, might need to pass it or rely on what's available
}
if !user.facebook_click_id.present? ||
!APP_CONFIG.facebook_ad_source.include?(user.origin_utm_source)
return
end
# Enhance user data with hashed PII if available
# Facebook requires SHA256 hashing for PII
# For now, we rely on click_id and browser_id as primary keys if available
user_data[:fbc] = user.facebook_click_id if user.facebook_click_id.present?
user_data[:fbp] = user.facebook_browser_id if user.facebook_browser_id.present?
# If we have email/phone, we should hash them. But for now, let's stick to the IDs.
# Ideally we should hash email if we have it.
if user.email.present?
user_data[:em] = Digest::SHA256.hexdigest(user.email.downcase)
end
payload = {
data: [
{
event_name: event_name,
event_time: Time.now.to_i,
action_source: "website",
user_data: user_data,
custom_data: custom_data,
event_source_url: event_source_url
}
]
}
if test_event_code.present?
payload[:test_event_code] = test_event_code
end
url = URI.parse("https://graph.facebook.com/v19.0/#{pixel_id}/events?access_token=#{access_token}")
http = Net::HTTP.new(url.host, url.port)
if APP_CONFIG.facebook_conversion_api_tls == false
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
else
http.use_ssl = true
end
request = Net::HTTP::Post.new(url.request_uri)
request['Content-Type'] = 'application/json'
request.body = payload.to_json
response = http.request(request)
if response.code.to_i >= 400
puts("CapiTransmitter Error: #{response.code} - #{response.body}")
else
puts("CapiTransmitter Success: #{event_name} sent for User #{user.id}")
end
rescue => e
puts("CapiTransmitter Exception: #{e.message}")
puts(e.backtrace.join("\n"))
end
end
end

View File

@ -4,20 +4,22 @@ module JamRuby
def self.send_reminders
begin
cutoff_date = Date.parse(Rails.application.config.profile_complete_reminders_effective_from_date) # Define a cutoff date for the profile completion emails
#If the user has not updated their profile 1 day after signup, then send reminder email1
reminder1_users.find_each do |user|
reminder1_users(cutoff_date).find_each do |user|
puts "reminder1_users user: #{user.id}"
UserMailer.profile_complete_reminder1(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder1_sent_at: Time.now)
end
#If the user has not updated their profile 3 days after signup, then send reminder email2
reminder2_users.find_each do |user|
reminder2_users(cutoff_date).find_each do |user|
UserMailer.profile_complete_reminder2(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder2_sent_at: Time.now)
end
#If the user has not updated their profile 5 days after signup, then send reminder email3
reminder3_users.find_each do |user|
reminder3_users(cutoff_date).find_each do |user|
UserMailer.profile_complete_reminder3(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder3_sent_at: Time.now)
end
@ -31,18 +33,20 @@ module JamRuby
User.where("users.profile_completed_at IS NULL AND users.subscribe_email = ?", true)
end
def self.reminder1_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NULL", 1.day.ago)
def self.reminder1_users(cutoff_date)
# ensure that the user has had the account for at least one day
puts "reminder1_users cutoff_date: #{cutoff_date}"
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder1_sent_at IS NULL AND (NOW() - users.created_at) > INTERVAL '1 day'", cutoff_date)
end
def self.reminder2_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", 3.days.ago)
def self.reminder2_users(cutoff_date)
# ensure that the user has had the account for at least three days and guard against rapid back-to-back emails
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder1_sent_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", cutoff_date, 2.days.ago)
end
def self.reminder3_users
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", 5.days.ago)
def self.reminder3_users(cutoff_date)
# ensure that user has had the profile for 5 days and guard against rapid back-to-back emails
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder2_sent_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", cutoff_date, 2.days.ago)
end
end
end

View File

@ -7,40 +7,40 @@ module JamRuby
begin
cutoff_date = Date.parse(Rails.application.config.gear_setup_reminders_effective_from_date) # Define a cutoff date for the survey/gear setup emails
reminder1_users(cutoff_date).find_each do |user|
reminder1_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.gear_setup_reminder1(user).deliver_now
User.where(id: user.id).update_all(gear_setup_reminder1_sent_at: Time.now)
end
reminder2_users(cutoff_date).find_each do |user|
reminder2_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.gear_setup_reminder2(user).deliver_now
User.where(id: user.id).update_all(gear_setup_reminder2_sent_at: Time.now)
end
reminder3_users(cutoff_date).find_each do |user|
reminder3_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.gear_setup_reminder3(user).deliver_now
User.where(id: user.id).update_all(gear_setup_reminder3_sent_at: Time.now)
end
rescue Exception => e
@@log.error("unable to send gear setup reminder email #{e}")
puts "unable to send gear setup reminder email #{e}"
@@log.error("unable to send GearSetupReminder email #{e}")
puts "unable to send GearSetupReminder email #{e}"
end
end
def self.prospect_users
User.where("users.first_certified_gear_at IS NULL")
User.where("users.first_certified_gear_at IS NULL AND users.subscribe_email = ?", true)
end
def self.reminder1_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL AND (NOW() - users.created_at) > INTERVAL '1 day'", cutoff_date)
end
def self.reminder2_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL AND users.gear_setup_reminder1_sent_at < ?", cutoff_date, 2.days.ago)
end
def self.reminder3_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL AND users.gear_setup_reminder2_sent_at < ?", cutoff_date, 2.days.ago)
end
end

View File

@ -0,0 +1,47 @@
module JamRuby
class GroupSessionReminder
@@log = Logging.logger[GroupSessionReminder]
def self.send_reminders
begin
cutoff_date = Date.parse(Rails.application.config.group_session_reminders_effective_from_date) # Define a cutoff date for the group session reminder emails
reminder1_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.group_session_reminder1(user).deliver_now
User.where(id: user.id).update_all(group_session_reminder1_sent_at: Time.now)
end
reminder2_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.group_session_reminder2(user).deliver_now
User.where(id: user.id).update_all(group_session_reminder2_sent_at: Time.now)
end
reminder3_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.group_session_reminder3(user).deliver_now
User.where(id: user.id).update_all(group_session_reminder3_sent_at: Time.now)
end
rescue Exception => e
@@log.error("unable to send GroupSessionReminder email #{e}")
puts "unable to send GroupSessionReminder email #{e}"
end
end
def self.prospect_users
User.where("users.first_certified_gear_at IS NOT NULL AND users.first_real_music_session_at IS NULL")
end
def self.reminder1_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.first_music_session_at IS NOT NULL AND users.group_session_reminder1_sent_at IS NULL", cutoff_date)
end
def self.reminder2_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder1_sent_at IS NOT NULL AND users.group_session_reminder2_sent_at IS NULL AND users.first_music_session_at < ? AND group_session_reminder1_sent_at < ?", cutoff_date, 3.days.ago, 1.day.ago)
end
def self.reminder3_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder2_sent_at IS NOT NULL AND users.group_session_reminder3_sent_at IS NULL AND users.first_music_session_at < ? AND group_session_reminder2_sent_at < ?", cutoff_date, 5.days.ago, 1.day.ago)
end
end
end

View File

@ -6,40 +6,40 @@ module JamRuby
def self.send_reminders
begin
cutoff_date = Date.parse(Rails.application.config.test_gear_reminders_effective_from_date) # Define a cutoff date for the test gear setup emails
reminder1_users.find_each(batch_size:100) do |user|
reminder1_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.test_gear_reminder1(user).deliver_now
User.where(id: user.id).update_all(test_gear_reminder1_sent_at: Time.now)
end
reminder2_users.find_each(batch_size:100) do |user|
reminder2_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.test_gear_reminder2(user).deliver_now
User.where(id: user.id).update_all(test_gear_reminder2_sent_at: Time.now)
end
reminder3_users.find_each(batch_size:100) do |user|
reminder3_users(cutoff_date).find_each(batch_size:100) do |user|
UserMailer.test_gear_reminder3(user).deliver_now
User.where(id: user.id).update_all(test_gear_reminder3_sent_at: Time.now)
end
rescue Exception => e
@@log.error("unable to send gear setup reminder email #{e}")
puts "unable to send gear setup reminder email #{e}"
@@log.error("unable to send TestGearReminder email #{e}")
puts "unable to send TestGearReminder email #{e}"
end
end
def self.prospect_users
User.where("users.first_music_session_at IS NULL")
User.where("users.first_music_session_at IS NULL AND first_certified_gear_at IS NOT NULL")
end
def self.reminder1_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at >= ? AND users.test_gear_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date)
TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder1_sent_at IS NULL AND (NOW() - users.first_certified_gear_at) > INTERVAL '1 day'", cutoff_date)
end
def self.reminder2_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at >= ? AND users.test_gear_reminder1_sent_at IS NOT NULL AND users.test_gear_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date)
TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder1_sent_at IS NOT NULL AND users.test_gear_reminder2_sent_at IS NULL AND users.test_gear_reminder1_sent_at < ?", cutoff_date, 2.days.ago)
end
def self.reminder3_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at > ? AND users.test_gear_reminder2_sent_at IS NOT NULL AND users.test_gear_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date)
TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder2_sent_at IS NOT NULL AND users.test_gear_reminder3_sent_at IS NULL AND users.test_gear_reminder2_sent_at < ?", cutoff_date, 2.days.ago)
end
end
end

View File

@ -0,0 +1,66 @@
# lib/jam_ruby/lib/trial_expires_reminder.rb
#
# Sends reminder emails after a user's free trial has ended.
#
# Usage: JamRuby::TrialExpiresReminder.send_reminders
#
module JamRuby
class TrialExpiresReminder
@@log = Logging.logger[TrialExpiresReminder]
FIRST_NOTIFICATION_CHECK = 1.day.ago
# unused here, but just as a form of simple documentation...
# SECOND_NOTIFICATION_CHECK = 4.days (from the previous reminder)
# THIRD_NOTIFICATION_CHECK = 4.days (from the previous reminder)
def self.prospect_users(cutoff_date)
User.where("(users.subscription_trial_ends_at IS NOT NULL AND users.created_at > ?)", cutoff_date)
end
# trial_ended | in_trial
def self.reminder1_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND users.subscription_trial_ends_at < ? AND users.trial_expires_reminder1_sent_at IS NULL", FIRST_NOTIFICATION_CHECK)
end
def self.reminder2_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND trial_expires_reminder1_sent_at IS NOT NULL AND users.trial_expires_reminder2_sent_at IS NULL AND (NOW() - trial_expires_reminder1_sent_at) > INTERVAL '4 days'")
end
def self.reminder3_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND trial_expires_reminder2_sent_at IS NOT NULL AND users.trial_expires_reminder3_sent_at IS NULL AND (NOW() - trial_expires_reminder2_sent_at) > INTERVAL '4 days'")
end
def self.send_reminders
cod = Date.parse(Rails.application.config.trial_expires_reminders_effective_from_date) # Define a cutoff date for the trial expires reminder emails
reminder1_users(cod).find_each(batch_size: 100) do |user|
begin
UserMailer.trial_expires_reminder1(user).deliver_now
user.update_attribute(:trial_expires_reminder1_sent_at, Time.now)
rescue Exception => e
@@log.error("unable to send trial_expires_reminder1 email #{e}")
puts "unable to send trial_expires_reminder1 email #{e}"
end
end
reminder2_users(cod).find_each(batch_size: 100) do |user|
begin
UserMailer.trial_expires_reminder2(user).deliver_now
user.update_attribute(:trial_expires_reminder2_sent_at, Time.now)
rescue Exception => e
@@log.error("unable to send trial_expires_reminder2 email #{e}")
puts "unable to send trial_expires_reminder2 email #{e}"
end
end
reminder3_users(cod).find_each(batch_size: 100) do |user|
begin
UserMailer.trial_expires_reminder3(user).deliver_now
user.update_attribute(:trial_expires_reminder3_sent_at, Time.now)
rescue Exception => e
@@log.error("unable to send trial_expires_reminder3 email #{e}")
puts "unable to send trial_expires_reminder3 email #{e}"
end
end
end
end
end

View File

@ -1,8 +1,7 @@
module JamRuby
class Ars < ActiveRecord::Base
attr_accessible :active, :name, :id_int, :ip, as: :admin
attr_accessible :password, :username, :active, :beta, :name, :provider, :id_int, :ip, :port, :continent, :country, :city, :subdivision, :latitude, :longitude, as: :admin
self.table_name = "arses"
@@log = Logging.logger[Ars]
@ -11,4 +10,4 @@ module JamRuby
Ars.where(active: true, beta: beta).where('ip is not NULL').where("ip != ''").all
end
end
end
end

View File

@ -76,7 +76,7 @@ module JamRuby
after_save :update_teacher_pct
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time,
:v2_photo_url, :v2_photo_uploaded
:v2_photo_url, :v2_photo_uploaded, :facebook_click_id, :facebook_browser_id
# updating_password corresponds to a lost_password
attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required, :user_type
@ -542,6 +542,21 @@ module JamRuby
@updating_progression_field = true
if self[field_name].nil?
self[field_name] = time
# CAPI Hooks
begin
case field_name.to_s
when 'first_ran_client_at'
# StartTrial: When user opens the app
CapiTransmitter.send_event('StartTrial', self, { value: '0.00', currency: 'USD', predicted_ltv: '0.00' })
when 'first_certified_gear_at'
# AddToCart: When user finishes Gear Wizard
CapiTransmitter.send_event('AddToCart', self)
end
rescue => e
puts("Error sending #{field_name} CAPI event: #{e.message}")
end
self.save
end
end
@ -1579,6 +1594,8 @@ module JamRuby
license_end = options[:license_end]
import_source = options[:import_source]
desired_plan_code = options[:desired_plan_code]
facebook_click_id = options[:facebook_click_id]
facebook_browser_id = options[:facebook_browser_id]
if desired_plan_code == ''
desired_plan_code = nil
@ -1640,16 +1657,24 @@ module JamRuby
musician = true
end
user.musician = !!musician
user.facebook_click_id = facebook_click_id
user.facebook_browser_id = facebook_browser_id
if origin
user.origin_utm_source = origin["utm_source"]
user.origin_utm_medium = origin["utm_medium"]
user.origin_utm_campaign = origin["utm_campaign"]
user.origin_id = origin["utm_id"]
user.origin_term = origin["utm_term"]
user.origin_content = origin["utm_content"]
user.origin_referrer = origin["referrer"]
else
user.origin_utm_source = 'organic'
user.origin_utm_medium = 'organic'
user.origin_utm_campaign = nil
user.origin_id = nil
user.origin_term = nil
user.origin_content = nil
user.origin_referrer = nil
end
@ -1865,6 +1890,15 @@ module JamRuby
if user.errors.any?
raise ActiveRecord::Rollback
else
# CAPI Hook: CompleteRegistration
if user.facebook_click_id.present?
begin
CapiTransmitter.send_event('CompleteRegistration', user)
rescue => e
puts("Error sending CompleteRegistration CAPI event: #{e.message}")
end
end
# if the partner ID was present and the partner doesn't already have a user associated, associate this new user with the affiliate partner
if affiliate_partner && affiliate_partner.partner_user.nil?
affiliate_partner.partner_user = user

View File

@ -336,6 +336,33 @@ module JamRuby
end
end
def report_meta_capi(current_user, plan_code)
# CAPI Hook: Subscribe
begin
monthly_cost = case plan_code
when JamRuby::SubscriptionDefinitions::JAM_SILVER, JamRuby::SubscriptionDefinitions::JAM_SILVER_YEARLY
5.00
when JamRuby::SubscriptionDefinitions::JAM_GOLD, JamRuby::SubscriptionDefinitions::JAM_GOLD_YEARLY
10.00
when JamRuby::SubscriptionDefinitions::JAM_PLATINUM, JamRuby::SubscriptionDefinitions::JAM_PLATINUM_YEARLY
20.00
else
0.00
end
ltv = monthly_cost * 12
begin
puts "Sending CAPI Subscribe event #{current_user.email}, #{monthly_cost}, #{ltv}"
CapiTransmitter.send_event('Subscribe', current_user, { value: monthly_cost.to_s, currency: 'USD', predicted_ltv: ltv.to_s })
rescue => e
puts "Error sending CAPI Subscribe event #{current_user.email}, #{e.message}"
end
rescue => e
puts "Error sending CAPI Subscribe event #{current_user.email}, #{e.message}"
end
end
def get_highest_plan(subscription)
SubscriptionDefinitions.higher_plan(subscription.plan.plan_code, get_pending_plan_code(subscription))
end
@ -364,6 +391,8 @@ module JamRuby
end
current_user.reset_playtime
current_user.save(validate: false)
report_meta_capi(current_user, plan_code)
rescue => e
puts "Could not create subscription for user #{current_user.email}. #{e}"
return false, subscription, account

View File

@ -17,6 +17,8 @@ module JamRuby
EmailSignupSurvey.send_survey
GearSetupReminder.send_reminders
TestGearReminder.send_reminders
GroupSessionReminder.send_reminders
TrialExpiresReminder.send_reminders
ConnectionManager.new.cleanup_dangling
@@log.info("done")

View File

@ -0,0 +1,139 @@
require "spec_helper"
describe TrialExpiresReminder do
let(:user1) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
let(:user3) { FactoryGirl.create(:user) }
before(:each) do
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
User.delete_all
UserMailer.deliveries.clear
Rails.application.config.trial_expires_reminders_effective_from_date = 2.weeks.ago.to_s
end
after(:each) do
ActionMailer::Base.deliveries.clear
end
def no_more_emails_sent
UserMailer.deliveries.clear
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(0)
end
it "sends reminder emails to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 1.days.from_now
user1.subscription_sync_code = 'trial_ended'
user1.save!
user2.subscription_trial_ends_at = 2.days.ago
user2.subscription_sync_code = 'trial_ended'
user2.save!
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(1)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user2.email)
# Check if the first reminder email was sent by verifying the subject
expect(ActionMailer::Base.deliveries.last.subject).to include("Your free gold trial has expired, but you have great options to keep playing!")
expect(user2.reload.trial_expires_reminder1_sent_at).not_to be_nil
no_more_emails_sent
end
it "does not send reminder emails to users who have already received them" do
user1.reload
user1.subscription_trial_ends_at = 1.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = Time.now
user1.save!
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(0)
end
it "sends the second reminder email to users whose trials are about to expire" do
user1.reload
user2.reload
# pretend that the first reminder email was sent 2 days ago
user1.subscription_trial_ends_at = 4.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = 5.days.ago
user1.save!
user2.subscription_trial_ends_at = 4.days.ago + 1.hour
user2.subscription_sync_code = 'trial_ended'
user2.trial_expires_reminder1_sent_at = Time.now
user2.save!
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(1)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email)
# Check if the second reminder email was sent by verifying the subject
expect(ActionMailer::Base.deliveries.last.subject).to include("Dont forget to check your options to keep playing")
expect(user1.reload.trial_expires_reminder2_sent_at).not_to be_nil
no_more_emails_sent
end
it "sends the third reminder email to users whose trials are about to expire" do
user1.reload
user1.subscription_trial_ends_at = 10.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = 8.days.ago
user1.trial_expires_reminder2_sent_at = 9.days.ago
user1.save!
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(1)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email)
# Check if the third reminder email was sent by verifying the subject
expect(ActionMailer::Base.deliveries.last.subject).to include("One last reminder!")
expect(user1.reload.trial_expires_reminder3_sent_at).not_to be_nil
no_more_emails_sent
end
it "sends first and second and third reminder emails to users whose trials are about to expire" do
user1.reload
user2.reload
user3.reload
user1.subscription_trial_ends_at = 2.days.ago
user1.subscription_sync_code = 'trial_ended'
user1.save!
user2.subscription_trial_ends_at = 2.days.ago
user2.trial_expires_reminder1_sent_at = 5.days.ago
user2.subscription_sync_code = 'trial_ended'
user2.save!
user3.subscription_trial_ends_at = 2.days.ago
user3.trial_expires_reminder1_sent_at = 8.days.ago
user3.trial_expires_reminder2_sent_at = 9.days.ago
user3.subscription_sync_code = 'trial_ended'
user3.save!
TrialExpiresReminder.send_reminders
expect(user1.reload.trial_expires_reminder1_sent_at).not_to be_nil
expect(user2.reload.trial_expires_reminder2_sent_at).not_to be_nil
expect(user3.reload.trial_expires_reminder3_sent_at).not_to be_nil
expect(ActionMailer::Base.deliveries.count).to eq(3)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email, user2.email, user3.email)
no_more_emails_sent
end
end

View File

@ -16,7 +16,7 @@ describe UserMailer do
UserMailer.deliveries.clear
end
describe "should send confirm email", focus: true do
describe "should send confirm email" do
let (:mail) { UserMailer.deliveries[0] }
let (:signup_confirmation_url) { "/confirm" }
@ -210,4 +210,227 @@ describe UserMailer do
# it { mail.text_part.body.include?("New JamKazam Musicians in your Area").should be_true }
# end
describe "sends group session reminder 1", focus: true do
let(:mail) { ActionMailer::Base.deliveries.last }
before(:each) do
ActionMailer::Base.deliveries.clear
UserMailer.group_session_reminder1(user).deliver_now
end
it "sends exactly one email" do
expect(ActionMailer::Base.deliveries.length).to eq(1)
end
it "has the correct from address" do
#expect(mail.from).to eq([UserMailer::DEFAULT_SENDER])
mail['from'].to_s.should == UserMailer::DEFAULT_SENDER
end
it "has the correct recipient" do
expect(mail.to).to eq([user.email])
end
it "is multipart" do
expect(mail).to be_multipart
end
it "has the expected subject" do
expect(mail.subject).to include("Its time to start playing music with others online!")
end
it "includes the correct content in the HTML part" do
expect(mail.html_part.body.to_s).to include("Now that you have set up your gear and checked it out in a solo session on your own")
end
it "includes the correct content in the text part" do
expect(mail.text_part.body.to_s).to include("Now that you have set up your gear and checked it out in a solo session on your own")
end
end
describe "sends group session reminder 2", focus: true do
let(:mail) { ActionMailer::Base.deliveries.last }
before(:each) do
ActionMailer::Base.deliveries.clear
UserMailer.group_session_reminder2(user).deliver_now
end
it "sends exactly one email" do
expect(ActionMailer::Base.deliveries.length).to eq(1)
end
it "has the correct from address" do
mail['from'].to_s.should == UserMailer::DEFAULT_SENDER
end
it "has the correct recipient" do
expect(mail.to).to eq([user.email])
end
it "is multipart" do
expect(mail).to be_multipart
end
it "has the expected subject" do
expect(mail.subject).to include("Take the last step to play online with others now, and experience the joy")
end
it "includes the correct content in the HTML part" do
expect(mail.html_part.body.to_s).to include("If you signed up for JamKazam to play music with others online")
end
it "includes the correct content in the text part" do
expect(mail.text_part.body.to_s).to include("If you signed up for JamKazam to play music with others online")
end
end
describe "sends group session reminder 3", focus: true do
let(:mail) { ActionMailer::Base.deliveries.last }
before(:each) do
ActionMailer::Base.deliveries.clear
UserMailer.group_session_reminder3(user).deliver_now
end
it "sends exactly one email" do
expect(ActionMailer::Base.deliveries.length).to eq(1)
end
it "has the correct from address" do
mail['from'].to_s.should == UserMailer::DEFAULT_SENDER
end
it "has the correct recipient" do
expect(mail.to).to eq([user.email])
end
it "is multipart" do
expect(mail).to be_multipart
end
it "has the expected subject" do
expect(mail.subject).to include("Dont waste your free 30-day premium gold planget in a session with others now!")
end
it "includes the correct content in the HTML part" do
expect(mail.html_part.body.to_s).to include("When you sign up for JamKazam, we give you a free 30-day premium")
end
it "includes the correct content in the text part" do
expect(mail.text_part.body.to_s).to include("When you sign up for JamKazam, we give you a free 30-day premium")
end
end
describe "sends trial expires reminder 1", focus: true do
let(:mail) { ActionMailer::Base.deliveries.last }
before(:each) do
ActionMailer::Base.deliveries.clear
UserMailer.trial_expires_reminder1(user).deliver_now
end
it "sends exactly one email" do
expect(ActionMailer::Base.deliveries.length).to eq(1)
end
it "has the correct from address" do
mail['from'].to_s.should == UserMailer::DEFAULT_SENDER
end
it "has the correct recipient" do
expect(mail.to).to eq([user.email])
end
it "is multipart" do
expect(mail).to be_multipart
end
it "has the expected subject" do
expect(mail.subject).to include("Your free gold trial has expired, but you have great options to keep playing!")
end
it "includes the correct content in the HTML part" do
expect(mail.html_part.body.to_s).to include("We hope youve enjoyed your 30-day free gold trial with JamKazam")
end
it "includes the correct content in the text part" do
expect(mail.text_part.body.to_s).to include("We hope youve enjoyed your 30-day free gold trial with JamKazam")
end
end
describe "sends trial expires reminder 2", focus: true do
let(:mail) { ActionMailer::Base.deliveries.last }
before(:each) do
ActionMailer::Base.deliveries.clear
UserMailer.trial_expires_reminder2(user).deliver_now
end
it "sends exactly one email" do
expect(ActionMailer::Base.deliveries.length).to eq(1)
end
it "has the correct from address" do
mail['from'].to_s.should == UserMailer::DEFAULT_SENDER
end
it "has the correct recipient" do
expect(mail.to).to eq([user.email])
end
it "is multipart" do
expect(mail).to be_multipart
end
it "has the expected subject" do
expect(mail.subject).to include("Dont forget to check your options to keep playing")
end
it "includes the correct content in the HTML part" do
expect(mail.html_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired")
end
it "includes the correct content in the text part" do
expect(mail.text_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired")
end
end
describe "sends trial expires reminder 3", focus: true do
let(:mail) { ActionMailer::Base.deliveries.last }
before(:each) do
ActionMailer::Base.deliveries.clear
UserMailer.trial_expires_reminder3(user).deliver_now
end
it "sends exactly one email" do
expect(ActionMailer::Base.deliveries.length).to eq(1)
end
it "has the correct from address" do
mail['from'].to_s.should == UserMailer::DEFAULT_SENDER
end
it "has the correct recipient" do
expect(mail.to).to eq([user.email])
end
it "is multipart" do
expect(mail).to be_multipart
end
it "has the expected subject" do
expect(mail.subject).to include("One last reminder!")
end
it "includes the correct content in the HTML part" do
expect(mail.html_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired")
end
it "includes the correct content in the text part" do
expect(mail.text_part.body.to_s).to include("Your 30-day free gold trial with JamKazam has expired")
end
end
end

View File

@ -55,6 +55,7 @@
//= require AAB_message_factory
//= require jam_rest
//= require ga
//= require meta_tracking
//= require utils
//= require subscription_utils
//= require profile_utils

View File

@ -13,6 +13,7 @@
//= require AAB_message_factory
//= require jam_rest
//= require ga
//= require meta_tracking
//= require layout
//= require jamkazam
//= require utils

View File

@ -8,5 +8,6 @@
//= require utils
//= require jam_rest
//= require ga
//= require meta_tracking
//= require corp/init
//= require_directory ../corp

View File

@ -64,6 +64,7 @@
//= require custom_controls
//= require jam_rest
//= require ga
//= require meta_tracking
//= require session_utils
//= require recording_utils
//= require helpBubbleHelper

View File

@ -42,6 +42,7 @@
//= require ui_helper
//= require jam_rest
//= require ga
//= require meta_tracking
//= require recordingModel
//= require web/signup_helper
//= require web/signin_helper

View File

@ -0,0 +1,117 @@
/**
* meta_tracking.js
* A standalone module to capture and persist Meta attribution signals (fbclid, _fbp) in cookies.
*
* Logic adapted for legacy environment (no React hooks).
* - Checks URL for `fbclid` and sets `_fbc` cookie.
* - Checks for `_fbp` cookie; if missing, generates and sets it.
*/
(function (window, document) {
'use strict';
var MetaTracking = {
init: function () {
var location = window.location;
this.handleFbc(location.search);
this.handleFbp();
this.handleUtm(location.search);
},
// 1. Parsing and storing _fbc (Click ID)
handleFbc: function (searchParams) {
var fbclid = this.getQueryParam('fbclid', searchParams);
if (fbclid) {
var version = 'fb';
var subdomainIndex = 1; // 1 = example.com
var creationTime = new Date().getTime(); // Unix timestamp in ms
// Format: fb.1.timestamp.id
var fbcValue = version + '.' + subdomainIndex + '.' + creationTime + '.' + fbclid;
this.setCookie('_fbc', fbcValue, 90);
}
},
handleUtm: function (searchParams) {
var self = this;
if (!searchParams) return;
// Logically, we want to capture all utm_ parameters.
// We can either iterate a list or dynamic regex.
// Given the requirement to be robust, let's look for "utm_"
var query = searchParams.substring(1); // remove '?'
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (pair.length === 2) {
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
if (key.indexOf('utm_') === 0) {
self.setCookie(key, value, 90);
}
}
}
},
// 2. Handling _fbp (Browser ID)
handleFbp: function () {
if (!this.getCookie('_fbp')) {
var version = 'fb';
var subdomainIndex = 1;
var creationTime = new Date().getTime();
var randomInt = Math.floor(Math.random() * 10000000000); // 10-digit random number
// Format: fb.1.timestamp.randomDigits
var fbpValue = version + '.' + subdomainIndex + '.' + creationTime + '.' + randomInt;
this.setCookie('_fbp', fbpValue, 90);
}
},
// Helper: Get query param by name
getQueryParam: function (name, search) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
},
// Helper: Set cookie
setCookie: function (name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
// Ensure path is root and domain is included if needed (defaults to current host)
document.cookie = name + "=" + (value || "") + expires + "; path=/";
},
// Helper: Get cookie
getCookie: function (name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
};
// Initialize on ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
MetaTracking.init();
} else {
// IE9+ support for DOMContentLoaded
document.addEventListener('DOMContentLoaded', function () {
MetaTracking.init();
});
}
})(window, document);

View File

@ -23,6 +23,7 @@
//= require AAB_message_factory
//= require jam_rest
//= require ga
//= require meta_tracking
//= require layout
//= require jamkazam
//= require utils
@ -37,11 +38,11 @@
function initializeInfluxDB() {
window.stats = new InfluxDB({
"host" : gon.global.influxdb_host,
"port" : gon.global.influxdb_port,
"username" : gon.global.influxdb_username,
"password" : gon.global.influxdb_password,
"database" : gon.global.influxdb_database
"host": gon.global.influxdb_host,
"port": gon.global.influxdb_port,
"username": gon.global.influxdb_username,
"password": gon.global.influxdb_password,
"database": gon.global.influxdb_database
});
window.stats.write = window.stats.writePoint;

View File

@ -619,7 +619,7 @@ profileUtils = context.JK.ProfileUtils
<a className={classNames(submitClassNames)} onClick={this.onSubmitForm}>SUBMIT CARD INFORMATION</a>
</div>`
else
header = 'You have have a payment method on file already.'
header = 'You have a payment method on file already.'
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
managedSubscriptionAction = `<a href="/client#/account/subscription" className="button-orange">MANAGE MY SUBSCRIPTION</a>`
actions = `<div className="actions">

View File

@ -60,6 +60,7 @@
//= require custom_controls
//= require jam_rest
//= require ga
//= require meta_tracking
//= require session_utils
//= require recording_utils
//= require helpBubbleHelper

View File

@ -1,7 +1,7 @@
class ApiJamTracksController < ApiController
# have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index, :public_show]
before_filter :api_any_user, :only => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :lookup_jam_track_right, :only => [:download, :enqueue, :show_jam_track_right, :mark_active, :download_stem]
before_filter :ip_blacklist, :only => [:download_stem, :download]
@ -23,6 +23,11 @@ class ApiJamTracksController < ApiController
@jam_track = JamTrack.find_by_plan_code!(params[:plan_code])
end
def public_show
@jam_track = JamTrack.find_by!(slug: params[:slug])
render "api_jam_tracks/show_for_client", :layout => nil
end
def index
data = JamTrack.index(params, any_user)
@jam_tracks, @next, @count = data[0], data[1], data[2]

View File

@ -820,10 +820,6 @@ class ApiMusicSessionsController < ApiController
return render json: { code: "invalid_token", message: "No token found for '#{params[:token]}'" }, status: :forbidden
end
if token.expired_at < Time.now.utc
return render json: {code: "expired_token", message: "The token has expired" }, status: :forbidden
end
if token.token == Rails.application.config.video_open_room
return render json: { name: token.user.name, user_id: token.user.id }, status: 200
end

View File

@ -41,7 +41,9 @@ class ApiRecurlyController < ApiController
reuse_card: reuse_card_next_time,
affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie,
timezone: current_timezone
timezone: current_timezone,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp]
}
options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options)

View File

@ -158,7 +158,9 @@ class ApiUsersController < ApiController
affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie,
test_drive_package: params[:test_drive_package],
timezone: current_timezone
timezone: current_timezone,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp]
}
options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options)
@ -615,6 +617,15 @@ class ApiUsersController < ApiController
# NOTE: if you change confirm_email_link value below, you break outstanding email changes because links in user inboxes are broken
confirm_email_link = confirm_email_url + "?token="
do_bigin_complete_email(confirm_email_link)
end
def begin_update_email_alt
confirm_email_link = ApplicationHelper.spa_base_uri + '/confirm-email-change' + "?token="
do_bigin_complete_email(confirm_email_link)
end
def do_bigin_complete_email(confirm_email_link)
current_user.begin_update_email(params[:update_email], params[:current_password], confirm_email_link)
if current_user.errors.any?

View File

@ -49,11 +49,22 @@ class ApplicationController < ActionController::Base
def origin_cookie
begin
JSON.parse(cookies[:origin]) if cookies[:origin]
data = JSON.parse(cookies[:origin]) if cookies[:origin]
rescue
nil
data = nil
end
# Backfill with individual UTM cookies if present
# This supports cases where the frontend (jam-ui/web) set specific cookies
# or if the JSON cookie is missing/incomplete.
%w(utm_source utm_medium utm_campaign utm_term utm_content utm_id).each do |key|
if cookies[key].present?
data ||= {}
data[key] = cookies[key]
end
end
data
end
def track_origin

View File

@ -635,6 +635,10 @@ class LandingsController < ApplicationController
musician: true,
timezone: current_timezone,
first_name: @first,
origin: origin_cookie,
affiliate_referral_id: cookies[:affiliate_visitor],
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp],
instruments: [{:instrument_id => @instrument, :proficiency_level => 1, :priority => 1}])
if @user.errors.any?
first = @user.errors.first

View File

@ -120,7 +120,10 @@ class SessionsController < ApplicationController
last_name: auth_hash[:info][:last_name],
email: auth_hash[:info][:email],
timezone: current_timezone,
affiliate_referral_id: cookies[:affiliate_visitor])
affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp])
# Users who sign up using oauth are presumed to have valid email adddresses.
user.confirm_email!
@ -196,7 +199,9 @@ class SessionsController < ApplicationController
location: {:country => nil, :state => nil, :city => nil},
affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie,
timezone: current_timezone
timezone: current_timezone,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp]
}
options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options)

View File

@ -192,7 +192,11 @@ class UsersController < ApplicationController
affiliate_partner: @affiliate_partner,
timezone: current_timezone,
origin: origin_cookie,
desired_plan_code: desired_plan_code )
timezone: current_timezone,
origin: origin_cookie,
desired_plan_code: desired_plan_code,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp])
rend = _render('new')
# check for errors
@ -439,18 +443,21 @@ JS
end
def unsubscribe
unless @user = User.read_access_token(params[:user_token])
redirect_to '/'
end if params[:user_token].present?
unless params[:user_token].present? && (@user = User.read_access_token(params[:user_token]))
respond_to do |format|
format.html { redirect_to '/', alert: 'Invalid or expired token.' }
format.json { render json: { status: 'error', message: 'Invalid or expired token.' }, status: :unprocessable_entity }
end
return
end
#if request.get?
@user.subscribe_email = false
@user.save!
#elsif request.post?
@user.subscribe_email = false
@user.save!
#end
render text: 'You have been unsubscribed.'
respond_to do |format|
format.html { render plain: 'You have been unsubscribed.' }
format.json { render json: { status: 'success', message: 'You have been unsubscribed.' } }
end
end
def unsubscribe_user_match

View File

@ -10,10 +10,6 @@ node :jmep do |jam_track|
jam_track.jmep_json ? jam_track.jmep_json : nil
end
node :can_download do |item|
!!item.right_for_user(current_user, ShoppingCart::JAMTRACK_FULL)
end
child(:jam_track_tracks => :tracks) {
attributes :id, :part, :instrument, :track_type, :position
@ -27,22 +23,28 @@ child(:jam_track_tracks => :tracks) {
end
}
node do |jam_track|
right = jam_track.right_for_user(current_user)
if right
{
jam_track_right_id: right.id,
purchased_at: right.created_at.to_i,
last_mixdown_id: right.last_mixdown_id,
last_stem_id: right.last_stem_id
}
if current_user
node do |jam_track|
right = jam_track.right_for_user(current_user)
if right
{
jam_track_right_id: right.id,
purchased_at: right.created_at.to_i,
last_mixdown_id: right.last_mixdown_id,
last_stem_id: right.last_stem_id
}
end
end
end
node :mixdowns do |jam_track|
items = []
jam_track.mixdowns_for_user(current_user).each do |mixdown|
items << partial("api_jam_track_mixdowns/show", :object => mixdown)
node :mixdowns do |jam_track|
items = []
jam_track.mixdowns_for_user(current_user).each do |mixdown|
items << partial("api_jam_track_mixdowns/show", :object => mixdown)
end
items
end
node :can_download do |item|
!!item.right_for_user(current_user, ShoppingCart::JAMTRACK_FULL)
end
items
end

View File

@ -1,7 +1,11 @@
object @profile
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate,
:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp, :v2_photo_url, :v2_photo_uploaded
:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp, :v2_photo_url, :v2_photo_uploaded, :has_stored_credit_card?
node :has_recurly_account do |user|
user.recurly_code == user.id
end
child :online_presences => :online_presences do
attributes :id, :service_type, :username

View File

@ -1,3 +1,3 @@
object @jam_track
attributes :id, :name
attributes :id, :name, :original_artist

View File

@ -172,6 +172,12 @@ if defined?(Bundler)
config.google_secret = 'UwzIcvtErv9c2-GIsNfIo7bA'
config.google_email = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8@developer.gserviceaccount.com'
config.google_public_server_key = "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo"
# Facebook Conversions API
config.facebook_ad_source = Set.new(['facebook', 'instagram', 'meta', 'meta_ads', 'fb', 'ig', 'th', 'threads'])
config.facebook_pixel_id = "25663975269956141" # ONLY ONE. NO MULTI ENV
config.facebook_access_token = "EABAGQTtcGk4BQewKCztlfImT86ZAPsRAm3KDrYWsOFZAdLtJCa7bVOzdnZCnHZBFZBRMFVoSZCQdIpX01xXDM0ZCFmYrkPuAHdLsNBU61c7MZCZBLCFd3Q0qrxiJkeYr3F8FH3p98d0tGKljplhJ8vWBIKAiBXaUCCYk6urZB5qZAIEPsppWZCHOep91xZCtMVqtbGJQfFAZDZD" # ONLY ONE. NO MULTI ENV
config.facebook_conversion_api_tls = true
# 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'
@ -521,9 +527,14 @@ if defined?(Bundler)
config.user_match_monitoring_email = "user_match_monitoring_email@jamkazam.com"
config.send_user_match_mail_only_to_jamkazam_team = true
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10"
config.profile_complete_reminders_effective_from_date = "2025-06-10"
config.gear_setup_reminders_effective_from_date = "2025-06-10"
config.test_gear_reminders_effective_from_date = "2025-07-24"
config.group_session_reminders_effective_from_date = "2025-08-12"
config.trial_expires_reminders_effective_from_date = "2025-08-17"
config.action_mailer.asset_host = config.action_controller.asset_host
end
end

View File

@ -128,6 +128,9 @@ SampleApp::Application.configure do
config.send_user_match_mail_only_to_jamkazam_team = false
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10"
config.profile_complete_reminders_effective_from_date = "2025-06-10"
config.gear_setup_reminders_effective_from_date = "2025-06-10"
config.test_gear_reminders_effective_from_date = "2025-07-24"
config.group_session_reminders_effective_from_date = "2025-08-12"
config.trial_expires_reminders_effective_from_date = "2025-08-17"
end

View File

@ -4,7 +4,7 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
resource '/api/*',
headers: :any,
methods: [:get, :post, :delete, :options],
methods: [:get, :post, :put, :delete, :options],
credentials: true
end
@ -16,4 +16,14 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
methods: [:get],
credentials: true
end
allow do
origins Rails.configuration.spa_origin_url
resource '/unsubscribe/*',
headers: :any,
methods: [:get, :post, :put, :delete, :options],
credentials: true
end
end

View File

@ -84,7 +84,7 @@ en:
regards: "Best Regards,"
signature: "JamKazam Team"
profile_complete_reminder2:
subject: "Complete your JamKazam profile"
subject: "Take 2 minutes to fill out your JamKazam profile now"
greeting: "Hello"
paragraph1: "We share your profile with JamKazam members who may be a good fit to play music with you during your first week on the platform via an automated email feature. Dont miss this opportunity to connect. Click the button below to update your profile now, before its shared with others!"
update_profile: "Update Profile"
@ -92,7 +92,7 @@ en:
regards: "Best Regards,"
signature: "JamKazam Team"
profile_complete_reminder3:
subject: "Complete your JamKazam profile"
subject: "Last reminder to update your JamKazam profile"
greeting: "Hello"
paragraph1: "Your profile is your key to connecting with other musicians on JamKazam. It lets others know what instruments you play at what level of proficiency and what kinds of music you like to play. This lets our existing community find and reach out to you to connect and play, and this information also powers our automated matchmaking features that make recommendations to you. If you think you might want to play music live online on JamKazam, please click the button below now to fill out your profile!"
update_profile: "Update Profile"
@ -196,6 +196,116 @@ en:
If you have any trouble or feel confused while testing things out, you can email us for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>. You can also visit with a JamKazam support team member in our <a href="https://us02web.zoom.us/j/5967470315?pwd=eHZZL2hmVW1haUU5aTZTUUJobjFIdz09">weekly Zoom office hours</a>, which is offered every Wednesday from 11am to 12pm US Central Time.
regards: "Best Regards,"
signature: "JamKazam Team"
group_session_reminder1:
subject: "Its time to start playing music with others online!"
greeting: "Hello"
paragraph1: |
Now that you have set up your gear and checked it out in a solo session on your own, you are ready to get into the fun part actually playing music online with other musicians! If youre not sure how to go about this, here are a few good ways to get connected and start playing with others.
office_hours: Office Hours
paragraph2: |
JamKazam hosts an “office hours” session every Tuesday 7-8pm US Central Time. You can join this JamKazam session as a safe space with a JamKazam support team member to experience and learn how to play in online sessions, or to ask questions about particular features or issues youre having. To find this session, start the JamKazam app, click the Find Session tile, then click the Open Jams tile, and look for the session called “JamKazam Office Hours”, and click the Join icon on this session.
hosted_sessions: Hosted Sessions
paragraph3: |
Members of the JamKazam community host sessions regularly across a variety of genres and artists (e.g. blues, jazz, bluegrass, country, metal, etc). These are public sessions, intended for anyone to join. To see what sessions are upcoming, go to the JamKazam blog here: <a href="https://jamkazam.com/blog/">https://jamkazam.com/blog/</a>. Click on the most recent JamKazam News post, and scroll down to find the Upcoming Events section. Youll find a list of hosted sessions with dates/times, and you can click the link of any hosted session for a description and instructions on how to find and join the hosted session.
inviting_your_friends: Inviting Your Friends
paragraph4: |
If you have bandmates or friends who play, invite them to sign up at https://jamkazam.com. Our support team can help them get ready to go, and then youll be able to play with people you already know and love.
finding_good_matches: Finding Good Matches in the JamKazam Community
paragraph5: |
If you dont already have bandmates or musical friends, or if you do but would like to continue to grow your musical network and connections, we can help you connect with the “right” musicians who are already here in the JamKazam community. JamKazam has a unique feature you can use to search/filter our community by: 1) instruments played and skill level, 2) preferred genres, 3) date last active, and 4) latency so you can make sure youll have good sessions with minimal lag. Once you find musicians who area good match, you can view the details of their profiles and message them to introduce yourself and see if theyd like to try playing together in sessions. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000526937">Read this help article</a> to learn how to use these search and messaging features.
scheduling_sessions: Scheduling Sessions
paragraph6: |
Another way to find others who want to play the kinds of music youre interested in is to schedule future sessions and see if others in the JamKazam community RSVP to join you in your session. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124830">Read this help article</a> to learn how to use this feature.
asking_for_help: Asking Us For Help
paragraph7: |
If youre having trouble finding good musical matches and connections, you can always ask us for help. To do this, either open a help ticket here: <a href="https://www.jamkazam.com/help_desk">https://www.jamkazam.com/help_desk</a>. Or send us an email at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>. We can reach out into our community to help you try to find the right musicians. In your message, please tell us what genre(s) and what specific musical artists music you want to play, what other instruments youre looking for, and what skill level (e.g. beginner, intermediate, advanced) youre looking for.
paragraph8: |
As a final suggestion, dont be shy! If youre worried you wont play well enough, or you might get embarrassed, please set those intrusive thoughts aside. The musicians in the JamKazam community are very friendly, patient, supportive, and welcoming. This is the culture we all aspire to, and youll find it here 99% of the time (theres always that one guy, right?). Reach out and make new musical friends. Youll be glad you did!
regards: "Best Regards,"
signature: "JamKazam Team"
group_session_reminder2:
subject: "Take the last step to play online with others now, and experience the joy"
greeting: "Hello"
paragraph1: |
If you signed up for JamKazam to play music with others online, youre now just one step short of doing it. Take that step now to experience the joy of playing more often, to build new musical friendships, and to develop your musical skills! Theres so much to gain, and really nothing at all to lose. We have many stories of years-long friendships that have developed on JamKazam and resulted in plane trips and meetups in the real world to hang out, visit, and play.
paragraph2: |
Dont let any worries get in your way. The musicians in the JamKazam community are very friendly, patient, supportive, and welcoming. Reach out and make new musical friends. Youll be glad you did!
paragraph3: |
We sent you an email a couple days ago with several suggestions on great ways to get connected. Refer back to that email for all those detailed instructions and tips. And if you get stuck, dont hesitate to reach out to us to ask for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>.
regards: "Best Regards,"
signature: "JamKazam Team"
group_session_reminder3:
subject: "Dont waste your free 30-day premium gold planget in a session with others now!"
greeting: "Hello"
paragraph1: |
When you sign up for JamKazam, we give you a free 30-day premium gold plan so you can fully experience how amazing our online sessions are and how JamKazam can help you play more music with more people to bring more musical joy to your life.
paragraph2: |
Dont waste your 30-day window! Get connected to other musicians who want to play the kinds of music youre into, and get rolling. We sent you an email a couple days ago with several suggestions on great ways to get connected. Refer back to that email for all those detailed instructions and tips.
paragraph3: |
And if you get stuck, dont hesitate to reach out to us to ask for help at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a>.
regards: "Best Regards,"
signature: "JamKazam Team"
trial_expires_reminder1:
subject: "Your free gold trial has expired, but you have great options to keep playing!"
greeting: "Hello"
paragraph1: |
We hope youve enjoyed your 30-day free gold trial with JamKazam. If youve used it to play music in online sessions with others, below is a summary of your options to keep playing. If you havent finished setting up your gear or havent played in sessions with others yet, please send us an email at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> to let us know where youre stuck. We have a team that would love to help you!<br />
<ul>
<li>
<strong>Free Plan </strong> You can keep playing free on JamKazam with your current account. Our free plan lets you play 4 hours per month.
</li>
<li>
<strong>Premium Plans </strong> You can also subscribe to our silver, gold, or platinum plans, starting at $4.99/month, with upgrades to unlimited play time, higher audio quality, HD video, and access to premium features like audio and video recording, session broadcasting, and much more. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535">Read our help article on plans</a> to get all the details and see whats right for you.
</li>
</ul><br />
paragraph2: |
If youd like to take advantage of one of our premium plans, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529807">check this help article</a> that explains how to set up a payment method. Then <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529117">review this help article</a> that explains how to select your premium plan.
regards: "Best Regards,"
signature: "JamKazam Team"
trial_expires_reminder2:
subject: "Dont forget to check your options to keep playing"
greeting: "Hello"
paragraph1: |
Your 30-day free gold trial with JamKazam has expired, but you have great options to continue playing music online. <br />
<ul>
<li>
<strong>Free Plan </strong> You can keep playing free on JamKazam with your current account. Our free plan lets you play 4 hours per month.
</li>
<li>
<strong>Premium Plans </strong> You can also subscribe to our silver, gold, or platinum plans, starting at $4.99/month, with upgrades to unlimited play time, higher audio quality, HD video, and access to premium features like audio and video recording, session broadcasting, and much more. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535">Read our help article on plans</a> to get all the details and see whats right for you.
</li>
</ul><br />
paragraph2: |
If youd like to take advantage of one of our premium plans, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529807">check this help article</a> that explains how to set up a payment method. Then <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529117">review this help article</a> that explains how to select your premium plan.
paragraph3: |
If you havent finished setting up your gear or havent played in sessions with others yet, please send us an email at <a href="mailto:support@jamkazam.com">support@jamkazam.com</a> to let us know where youre stuck. We have a team that would love to help you!
regards: "Best Regards,"
signature: "JamKazam Team"
trial_expires_reminder3:
subject: "One last reminder!"
greeting: "Hello"
paragraph1: |
Your 30-day free gold trial with JamKazam has expired, but you have great options to continue playing music online.
<ul>
<li>
<strong>Free Plan </strong> You can keep playing free on JamKazam with your current account. Our free plan lets you play 4 hours per month.
</li>
<li>
<strong>Premium Plans </strong> You can also subscribe to our silver, gold, or platinum plans, starting at $4.99/month, with upgrades to unlimited play time, higher audio quality, HD video, and access to premium features like audio and video recording, session broadcasting, and much more. <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122535">Read our help article on plans</a> to get all the details and see whats right for you.
</li>
</ul>
paragraph2: |
If youd like to take advantage of one of our premium plans, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529807">check this help article</a> that explains how to set up a payment method. Then <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000529117">review this help article</a> that explains how to select your premium plan.
paragraph3: |
If you havent finished setting up your gear or havent played in sessions with others yet, please send us an email at support@jamkazam.com to let us know where youre stuck. We have a team that would love to help you!
regards: "Best Regards,"
signature: "JamKazam Team"
signup_survey:
subject: "Let us help you to be successful on JamKazam"
greeting: "Hi"

View File

@ -328,6 +328,7 @@ Rails.application.routes.draw do
match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased'
match '/jamtracks/artists' => 'api_jam_tracks#artist_index', :via => :get, :as => 'api_jam_tracks_list_artists'
match '/jamtracks/:id' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show'
match '/jamtracks/public/:slug' => 'api_jam_tracks#public_show', :via => :get, :as => 'api_jam_tracks_public_show'
match '/jamtracks/band/:plan_code' => 'api_jam_tracks#show_with_artist_info', :via => :get, :as => 'api_jam_tracks_show_with_artist_info'
match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list'
match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download'
@ -511,6 +512,7 @@ Rails.application.routes.draw do
# user account settings
match '/users/:id/update_email' => 'api_users#begin_update_email', :via => :post, :as => 'begin_update_email'
match '/users/:id/update_email_alt' => 'api_users#begin_update_email_alt', :via => :post, :as => 'begin_update_email_alt'
match '/users/update_email/:token' => 'api_users#finalize_update_email', :via => :post, :as => 'finalize_update_email'
# user profile

File diff suppressed because it is too large Load Diff

View File

@ -466,5 +466,158 @@ namespace :jam_tracks do
end
end
task gen_jamtrack_manifest: :environment do |task, arg|
# DOWNLOAD_AUDIO =
max = ENV['MAX_JAMTRACKS'].to_i
if max == 0
max = nil
end
puts "MAX JAMTRACKS #{max}"
jam_tracks = JamTrack.includes([:jam_track_tracks,
{genres_jam_tracks: :genres},
{jam_track_tracks: :instrument},
:genres]).where(status: 'Production').order('original_artist, name')
private_bucket = Rails.application.config.aws_bucket
s3_manager = S3Manager.new(private_bucket, Rails.application.config.aws_access_key_id, Rails.application.config.aws_secret_access_key)
tmp_dir = Dir.mktmpdir
FileUtils.mkdir_p tmp_dir
puts "tmp_dir=#{tmp_dir}"
csv_file = File.join(tmp_dir, "manifest.csv")
top_folder = File.join(tmp_dir, "audio")
FileUtils.mkdir_p(top_folder)
CSV.open(csv_file, "wb") do |csv|
header = ['JamTrackId', 'TrackId', 'Artist', 'Song', 'Instrument', 'Part', 'Type', 'Genre', 'LocalOgg', 'LocalMeta', 's3_path_url_44', 's3_path_url_48', 'AudioExists']
csv << header
jam_tracks.each do |jam_track|
song = jam_track.name
jam_track.jam_track_tracks.each do |jam_track_track|
instrument = jam_track_track.instrument_id
part = jam_track_track.part ? jam_track_track.part : ''
# construct the meta file for this track:
meta = {}
meta[:jam_track_id] = jam_track.id
meta[:track_id] = jam_track_track.id
meta[:artist] = jam_track.original_artist
meta[:song] = jam_track.name
meta[:instrument] = instrument
meta[:part] = part
meta[:type] = jam_track_track.track_type
genre = jam_track.genres.first
meta[:genre] = genre ? genre.description : ''
meta[:s3_path_url_44] = "s3://#{private_bucket}/" + jam_track_track.url_44
meta[:s3_path_url_48] = "s3://#{private_bucket}/" + jam_track_track.url_48
meta_json = File.join(tmp_dir, "meta_#{jam_track.id}_#{jam_track_track.id}.json")
File.open(meta_json, "w") do |f|
f.write(JSON.pretty_generate(meta))
end
# find the first a-z, 0-9 character and use that
first_character = song[0].downcase
folder = File.join(top_folder, first_character)
if File.exist?(folder) == false
FileUtils.mkdir_p(folder)
end
# folder structure is:
# audio/a/song-instrument-part-type.ogg
# audio/a/song-instrument-part-type.ogg.meta
# run 'part' through a sanitizer to make it filesystem safe
part = part.gsub(/[^0-9A-Za-z]/, '_')
base_name = File.join(folder, "#{jam_track.id}_#{jam_track_track.id}_#{instrument}_#{part}_#{jam_track_track.track_type}".downcase)
ogg_file = "#{base_name}.ogg"
exists = s3_manager.exists?(jam_track_track.url_48)
row = []
row << jam_track.id
row << jam_track_track.id
row << jam_track.original_artist
row << jam_track.name
row << instrument
row << part
row << jam_track_track.track_type
genre = jam_track.genres.first
row << (genre ? genre.description : '')
row << ogg_file.gsub(tmp_dir, '')
row << ogg_file.gsub(tmp_dir, '') + ".meta"
row << "s3://#{private_bucket}/" + jam_track_track.url_44
row << "s3://#{private_bucket}/" + jam_track_track.url_48
row << exists
csv << row
FileUtils.cp(meta_json, "#{ogg_file}.meta")
# cleanup meta
FileUtils.rm(meta_json)
end
if max > 0
max -= 1
if max == 0
puts "Max of jamtracks reached"
break
end
end
end
end
# dump the 1st 10 lines to stdout for quick verification
File.open(csv_file, "r") do|f|
10.times do |i|
puts f.readline
end
end
s3_manager.upload('jam_track_manifests/manifest.csv', csv_file, content_type: 'text/csv')
folder_to_zip = top_folder
output_tar_path = Dir.mktmpdir
output_tar_file = "#{output_tar_path}/archive.tar.gz"
safe_output = Shellwords.escape(output_tar_file)
# -c = create
# -z = compress with gzip
# -f = to a file
# -P = (Optional, but useful) Preserve absolute paths.
# By default, 'tar' strips the leading '/' for security.
# A common pattern is to cd into the parent dir first.
# Safer way: cd to the directory to get relative paths
parent_dir = File.dirname(folder_to_zip)
folder_name = File.basename(folder_to_zip)
safe_parent = Shellwords.escape(parent_dir)
safe_folder_name = Shellwords.escape(folder_name)
# This command is safer as it creates the archive with relative paths
command = "tar -czf #{safe_output} -C #{safe_parent} #{safe_folder_name}"
success = system(command)
if success
puts "Successfully created tar.gz file."
else
puts "Failed to create tar.gz file."
end
s3_manager.upload('jam_track_manifests/all-tracks.tar.gz', safe_output, content_type: 'application/gzip')
puts "tar.gz output=#{safe_output}"
puts "tmp_dir=#{tmp_dir}"
end
end

View File

@ -0,0 +1,27 @@
namespace :capi do
desc "Test Facebook CAPI connection"
task :test_connection => :environment do
puts "Testing CAPI connection..."
pixel_id = APP_CONFIG.facebook_pixel_id
access_token = APP_CONFIG.facebook_access_token
puts "Pixel ID: #{pixel_id}"
puts "Access Token: #{access_token.try(:truncate, 10)}"
if pixel_id.blank? || access_token.blank? || access_token == 'placeholder'
puts "WARNING: Configuration missing or placeholder."
end
# Mock user
user = User.last
user = User.find_by_email('seth@jamkazam.com')
if user
puts "Sending test event 'StartTrial' for User ID: #{user.id}"
#CapiTransmitter.send_event('StartTrial', user, { value: '0.00', currency: 'USD', predicted_ltv: '0.00' }, nil, ENV['TEST_EVENT_CODE'])
CapiTransmitter.send_event('AddToCart', user)
puts "Check Rails log for output."
else
puts "No user found in database."
end
end
end

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