Compare commits
78 Commits
5645-payme
...
develop
| Author | SHA1 | Date |
|---|---|---|
|
|
dfe5facad6 | |
|
|
abae34c108 | |
|
|
df8cf11e37 | |
|
|
7baf003b90 | |
|
|
1944602b97 | |
|
|
86e151d239 | |
|
|
6a1506bbe8 | |
|
|
64a93dd42f | |
|
|
eb52813822 | |
|
|
8613a03d00 | |
|
|
8f0b8929ba | |
|
|
08e1c5274d | |
|
|
5f347ccfac | |
|
|
ca2bf19b7a | |
|
|
fad1f9d6d9 | |
|
|
51838fb413 | |
|
|
45cb401112 | |
|
|
9078515984 | |
|
|
52ae83e2f4 | |
|
|
b454cf9ead | |
|
|
65f5624ff9 | |
|
|
e82450dfe9 | |
|
|
f1992eaa78 | |
|
|
bdeecc76c6 | |
|
|
8c8024c12b | |
|
|
c183af3d6b | |
|
|
ab424c21d2 | |
|
|
753f35b24d | |
|
|
624853c868 | |
|
|
71d8571eb9 | |
|
|
fae16c7483 | |
|
|
253a6c566b | |
|
|
ed561be4b5 | |
|
|
eb69640667 | |
|
|
9b0a9d1f32 | |
|
|
e1cc5483a2 | |
|
|
73d2a7a020 | |
|
|
eb298d6859 | |
|
|
93c4154648 | |
|
|
fbd871d204 | |
|
|
0d82f6ee16 | |
|
|
1279b16ec0 | |
|
|
e3cff0a825 | |
|
|
6a6e4cde09 | |
|
|
1dd15fb0aa | |
|
|
9282369e54 | |
|
|
828191d683 | |
|
|
c0031cfe3d | |
|
|
9b17546082 | |
|
|
eed3d51f4b | |
|
|
4eac68b645 | |
|
|
fe6157e8cf | |
|
|
675bf2b69c | |
|
|
4ffc0d9b3b | |
|
|
7b665325f7 | |
|
|
eab17b3340 | |
|
|
4fafe30141 | |
|
|
7f85c91601 | |
|
|
d424026f17 | |
|
|
a4f8935b3a | |
|
|
4f837ae67f | |
|
|
0053775c7e | |
|
|
9d6c71829f | |
|
|
d00a0c08f7 | |
|
|
7e2c917ca0 | |
|
|
a84a55f178 | |
|
|
25ecab2c65 | |
|
|
b2f344fd30 | |
|
|
3bba9ec619 | |
|
|
86e03e0ba7 | |
|
|
67fd15c75c | |
|
|
b7a41c6465 | |
|
|
a8d5b8e735 | |
|
|
f977b7298e | |
|
|
9a41e8a236 | |
|
|
3d113e3877 | |
|
|
c7e80a0694 | |
|
|
9058c8af1d |
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
name: Test Runner
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Runner is working!"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# trigger build
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -55,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'));
|
||||
|
|
@ -130,7 +133,6 @@ function JKDashboardMain() {
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
const app = window.JK.JamKazam();
|
||||
|
||||
const jamServer = new window.JK.JamServer(app, function (event_type) {
|
||||
|
|
@ -142,6 +144,8 @@ function JKDashboardMain() {
|
|||
const clientInit = new window.JK.ClientInit();
|
||||
clientInit.init();
|
||||
|
||||
app.initialize();
|
||||
|
||||
window.JK.JamServer.connect() // singleton here defined in JamServer.js
|
||||
.done(function () {
|
||||
console.log('Jamserver connected...');
|
||||
|
|
@ -318,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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ 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, updateBillingInfo, getUserDetail, getCountries } from '../../helpers/rest';
|
||||
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';
|
||||
|
|
@ -30,7 +30,6 @@ const JKPaymentMethod = () => {
|
|||
const [countries, setCountries] = useState([]);
|
||||
const labelClassName = 'ls text-600 font-weight-semi-bold mb-0';
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [billingDataLoaded, setBillingDataLoaded] = useState(false);
|
||||
const [isCardValid, setIsCardValid] = useState(false);
|
||||
const { greaterThan } = useResponsive();
|
||||
|
||||
|
|
@ -72,9 +71,8 @@ const JKPaymentMethod = () => {
|
|||
const userResp = await getUserDetail(options);
|
||||
const userData = await userResp.json();
|
||||
if (userData.has_recurly_account) {
|
||||
setHasStoredCreditCard(userData.has_stored_credit_card);
|
||||
setHasStoredCreditCard(userData['has_stored_credit_card?']);
|
||||
await populateBillingAddress();
|
||||
setBillingDataLoaded(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get user details:', error);
|
||||
|
|
@ -128,46 +126,36 @@ const JKPaymentMethod = () => {
|
|||
setValue('country', selectedOption.value);
|
||||
};
|
||||
|
||||
const recurlyContainerRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.recurly) return;
|
||||
if (!recurlyContainerRef.current || !window.recurly || recurlyConfigured.current) return;
|
||||
|
||||
if (recurlyConfigured.current) return;
|
||||
window.recurly.configure({ publicKey: process.env.REACT_APP_RECURLY_PUBLIC_API_KEY });
|
||||
const elements = window.recurly.Elements();
|
||||
const cardElement = elements.CardElement();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
const container = document.querySelector('#recurly-elements');
|
||||
console.log('Checking for Recurly Elements container:', container);
|
||||
if (container && window.recurly) {
|
||||
console.log('Initializing Recurly Elements...');
|
||||
window.recurly.configure({ publicKey: process.env.REACT_APP_RECURLY_PUBLIC_API_KEY });
|
||||
const elements = window.recurly.Elements();
|
||||
const cardElement = elements.CardElement();
|
||||
cardElement.attach('#recurly-elements');
|
||||
cardElement.attach(recurlyContainerRef.current);
|
||||
|
||||
cardElement.on('change', (event) => {
|
||||
if (event.complete) {
|
||||
setIsCardValid(true);
|
||||
} else if (event.error) {
|
||||
setIsCardValid(false);
|
||||
} else {
|
||||
setIsCardValid(false);
|
||||
}
|
||||
});
|
||||
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);
|
||||
//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;
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 100);
|
||||
elementsRef.current = elements;
|
||||
recurlyConfigured.current = true;
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return () => {
|
||||
// Optional cleanup if the component unmounts
|
||||
recurlyContainerRef.current.innerHTML = '';
|
||||
recurlyConfigured.current = false;
|
||||
};
|
||||
}, [recurlyContainerRef.current]);
|
||||
|
||||
const onPayPalError = (error) => {
|
||||
console.error('PayPal Error:', error);
|
||||
|
|
@ -181,6 +169,7 @@ const JKPaymentMethod = () => {
|
|||
|
||||
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);
|
||||
|
|
@ -196,20 +185,11 @@ const JKPaymentMethod = () => {
|
|||
};
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
//first update billing address
|
||||
setSubmitting(true);
|
||||
const resp = await updateBillingInfo(data)
|
||||
if (!resp.ok) {
|
||||
setSubmitting(false);
|
||||
const errorData = await resp.json();
|
||||
console.error('Error updating billing info:', errorData);
|
||||
toast.error(errorData.message || t('payment_method.alerts.billing_update_error'));
|
||||
return;
|
||||
}
|
||||
if (paymentMethod === 'paypal') {
|
||||
if (paymentMethod === 'paypal') { // PayPal payment method
|
||||
handoverToPaypal();
|
||||
return;
|
||||
} else {
|
||||
} else { // Credit Card payment method
|
||||
|
||||
if (!elementsRef.current) {
|
||||
console.error('Recurly elementsRef.current is not ready');
|
||||
|
|
@ -222,17 +202,10 @@ const JKPaymentMethod = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!isCardValid) {
|
||||
console.error('Card is not valid');
|
||||
toast.error(t('payment_method.validations.card.invalid'));
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
window.recurly.token(elementsRef.current, formRef.current, (err, token) => {
|
||||
if (err) {
|
||||
console.error('Recurly token error:', err);
|
||||
toast.error(err.message || t('payment_method.alerts.card_processing_error'));
|
||||
toast.error(t('payment_method.alerts.card_processing_error'));
|
||||
setSubmitting(false);
|
||||
} else {
|
||||
console.log('Recurly token:', token.id);
|
||||
|
|
@ -241,7 +214,7 @@ const JKPaymentMethod = () => {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const handoverToPaypal = () => {
|
||||
|
|
@ -264,7 +237,7 @@ const JKPaymentMethod = () => {
|
|||
<strong>{t('payment_method.help_text_no_card')}</strong>
|
||||
</span>
|
||||
)}
|
||||
{t('payment_method.help_text')}
|
||||
{t('payment_method.help_text')}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} ref={formRef}>
|
||||
|
|
@ -276,7 +249,7 @@ const JKPaymentMethod = () => {
|
|||
<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')}
|
||||
{t('payment_method.first_name')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -291,7 +264,7 @@ const JKPaymentMethod = () => {
|
|||
<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')}
|
||||
{t('payment_method.last_name')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -306,7 +279,7 @@ const JKPaymentMethod = () => {
|
|||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-md-right">
|
||||
<Label for="address1" className={labelClassName}>
|
||||
{t('payment_method.address1')}
|
||||
{t('payment_method.address1')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -321,7 +294,7 @@ const JKPaymentMethod = () => {
|
|||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-md-right">
|
||||
<Label for="address2" className={labelClassName}>
|
||||
{t('payment_method.address2')}
|
||||
{t('payment_method.address2')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -336,7 +309,7 @@ const JKPaymentMethod = () => {
|
|||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-md-right">
|
||||
<Label for="city" className={labelClassName}>
|
||||
{t('payment_method.city')}
|
||||
{t('payment_method.city')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -351,7 +324,7 @@ const JKPaymentMethod = () => {
|
|||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-md-right">
|
||||
<Label for="state" className={labelClassName}>
|
||||
{t('payment_method.state')}
|
||||
{t('payment_method.state')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -366,7 +339,7 @@ const JKPaymentMethod = () => {
|
|||
<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')}
|
||||
{t('payment_method.zip_code')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -384,7 +357,7 @@ const JKPaymentMethod = () => {
|
|||
<Row className="mb-2">
|
||||
<Col xs={12} md={5} lg={4} className="text-md-right">
|
||||
<Label for="country" className={labelClassName}>
|
||||
{t('payment_method.country')}
|
||||
{t('payment_method.country')}
|
||||
</Label>
|
||||
</Col>
|
||||
<Col>
|
||||
|
|
@ -447,7 +420,7 @@ const JKPaymentMethod = () => {
|
|||
</Row>
|
||||
<Row>
|
||||
<Col sm={8}>
|
||||
<div id="recurly-elements"></div>
|
||||
<div id="recurly-elements" ref={recurlyContainerRef}></div>
|
||||
{!isCardValid && errors.recurly && (
|
||||
<div className="text-danger">
|
||||
<small>{errors.recurly.message}</small>
|
||||
|
|
@ -479,7 +452,7 @@ const JKPaymentMethod = () => {
|
|||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={submitting || !billingDataLoaded }
|
||||
disabled={submitting}
|
||||
className="mt-3"
|
||||
>
|
||||
{submitting ? t('payment_method.submitting') : t('payment_method.save_payment_info')}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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));
|
||||
});
|
||||
|
|
@ -683,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));
|
||||
|
|
@ -700,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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -719,7 +717,7 @@ export const paypalPlaceOrder = (options = {}) => {
|
|||
|
||||
|
||||
export const submitStripe = (options = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
apiFetch(`/stripe`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(options)
|
||||
|
|
@ -749,4 +747,28 @@ export const updatePayment = (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));
|
||||
});
|
||||
}
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
}
|
||||
|
|
@ -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 />
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
class AddTestGearReminderColumnsToUsers < ActiveRecord::Migration
|
||||
def self.up
|
||||
execute "ALTER TABLE users ADD COLUMN test_gear_reminder1_sent_at TIMESTAMP"
|
||||
execute "ALTER TABLE users ADD COLUMN test_gear_reminder2_sent_at TIMESTAMP"
|
||||
execute "ALTER TABLE users ADD COLUMN test_gear_reminder3_sent_at TIMESTAMP"
|
||||
end
|
||||
|
||||
def self.down
|
||||
execute "ALTER TABLE users DROP COLUMN test_gear_reminder1_sent_at"
|
||||
execute "ALTER TABLE users DROP COLUMN test_gear_reminder2_sent_at"
|
||||
execute "ALTER TABLE users DROP COLUMN test_gear_reminder3_sent_at"
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -480,6 +480,78 @@ module JamRuby
|
|||
end
|
||||
end
|
||||
|
||||
def test_gear_reminder1(user)
|
||||
@user = user
|
||||
mail(:to => user.email, :subject => I18n.t('user_mailer.test_gear_reminder1.subject')) do |format|
|
||||
format.text
|
||||
format.html { render layout: "user_mailer_beta" }
|
||||
end
|
||||
end
|
||||
|
||||
def test_gear_reminder2(user)
|
||||
@user = user
|
||||
mail(:to => user.email, :subject => I18n.t('user_mailer.test_gear_reminder2.subject')) do |format|
|
||||
format.text
|
||||
format.html { render layout: "user_mailer_beta" }
|
||||
end
|
||||
end
|
||||
|
||||
def test_gear_reminder3(user)
|
||||
@user = user
|
||||
mail(:to => user.email, :subject => I18n.t('user_mailer.test_gear_reminder3.subject')) do |format|
|
||||
format.text
|
||||
format.html { render layout: "user_mailer_beta" }
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<p><%=I18n.t('user_mailer.test_gear_reminder1.greeting') -%> <%= @user.first_name -%> -</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph1').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph2').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph3').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph4').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph5').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.regards') -%>,<br />
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.signature') -%>
|
||||
</p>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<%=I18n.t('user_mailer.test_gear_reminder1.greeting') -%> <%= @user.first_name -%> -
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph1').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph2').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph3').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph4').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph5').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.regards') -%>,
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.signature') -%>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<p><%=I18n.t('user_mailer.test_gear_reminder2.greeting') -%> <%= @user.first_name -%> -</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph1').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph2').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph3').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph4').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.regards') -%>,<br />
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.signature') -%>
|
||||
</p>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<%=I18n.t('user_mailer.test_gear_reminder2.greeting') -%> <%= @user.first_name -%> -
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph1').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph2').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph3').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.paragraph4').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.regards') -%>,
|
||||
<%=I18n.t('user_mailer.test_gear_reminder2.signature') -%>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<p><%=I18n.t('user_mailer.test_gear_reminder3.greeting') -%> <%= @user.first_name -%> -</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.paragraph1').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.paragraph2').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.paragraph3').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.paragraph4').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.paragraph5').html_safe -%>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.regards') -%>,<br />
|
||||
<%=I18n.t('user_mailer.test_gear_reminder3.signature') -%>
|
||||
</p>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<%=I18n.t('user_mailer.test_gear_reminder1.greeting') -%> <%= @user.first_name -%> -
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph1').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph2').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph3').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph4').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.paragraph5').html_safe -%>
|
||||
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.regards') -%>,
|
||||
<%=I18n.t('user_mailer.test_gear_reminder1.signature') -%>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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') -%>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -5,42 +5,42 @@ module JamRuby
|
|||
|
||||
def self.send_reminders
|
||||
begin
|
||||
cutoff_date = Date.parse(Rails.application.config.signup_survey_cutoff_date) # Define a cutoff date for the survey/gear setup emails
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
module JamRuby
|
||||
class TestGearReminder
|
||||
|
||||
@@log = Logging.logger[TestGearReminder]
|
||||
|
||||
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(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(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(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 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 AND first_certified_gear_at IS NOT NULL")
|
||||
end
|
||||
|
||||
def self.reminder1_users(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.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.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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ module JamRuby
|
|||
EmailProfileReminder.send_reminders
|
||||
EmailSignupSurvey.send_survey
|
||||
GearSetupReminder.send_reminders
|
||||
TestGearReminder.send_reminders
|
||||
GroupSessionReminder.send_reminders
|
||||
TrialExpiresReminder.send_reminders
|
||||
ConnectionManager.new.cleanup_dangling
|
||||
|
||||
@@log.info("done")
|
||||
|
|
|
|||
|
|
@ -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("Don’t 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
|
||||
|
|
@ -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("It’s 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("Don’t waste your free 30-day premium gold plan–get 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 you’ve 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 you’ve 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("Don’t 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
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
//= require AAB_message_factory
|
||||
//= require jam_rest
|
||||
//= require ga
|
||||
//= require meta_tracking
|
||||
//= require utils
|
||||
//= require subscription_utils
|
||||
//= require profile_utils
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
//= require AAB_message_factory
|
||||
//= require jam_rest
|
||||
//= require ga
|
||||
//= require meta_tracking
|
||||
//= require layout
|
||||
//= require jamkazam
|
||||
//= require utils
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@
|
|||
//= require utils
|
||||
//= require jam_rest
|
||||
//= require ga
|
||||
//= require meta_tracking
|
||||
//= require corp/init
|
||||
//= require_directory ../corp
|
||||
|
|
@ -64,6 +64,7 @@
|
|||
//= require custom_controls
|
||||
//= require jam_rest
|
||||
//= require ga
|
||||
//= require meta_tracking
|
||||
//= require session_utils
|
||||
//= require recording_utils
|
||||
//= require helpBubbleHelper
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
//= require utils
|
||||
//= require subscription_utils
|
||||
//= require jamkazam
|
||||
//= require modern/JamServer_copy
|
||||
//= require JamServer
|
||||
|
||||
//= require fakeJamClient
|
||||
//= require fakeJamClientMessages
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
//= require custom_controls
|
||||
//= require jam_rest
|
||||
//= require ga
|
||||
//= require meta_tracking
|
||||
//= require session_utils
|
||||
//= require recording_utils
|
||||
//= require helpBubbleHelper
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
object @jam_track
|
||||
|
||||
attributes :id, :name
|
||||
attributes :id, :name, :original_artist
|
||||
|
|
@ -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,7 +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
|
||||
|
|
|
|||
|
|
@ -128,4 +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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -83,24 +83,21 @@ en:
|
|||
paragraph4: "For next steps, don’t forget you can always refer back to the Welcome email we sent you when you signed up. And if you run into any problems or get stuck, please send us email at support@jamkazam.com. We’re always happy to help"
|
||||
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. Don’t miss this opportunity to connect. Click the button below to update your profile now, before it’s shared with others!"
|
||||
update_profile: "Update Profile"
|
||||
paragraph2: "For next steps after your profile, don’t forget you can always refer back to the Welcome email we sent you when you signed up. And if you run into any problems or get stuck, please send us email at support@jamkazam.com. We’re always happy to help!"
|
||||
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"
|
||||
regards: "Best Regards,"
|
||||
signature: "JamKazam Team"
|
||||
|
||||
gear_setup_reminder1:
|
||||
subject: "Set up audio gear in JamKazam now so you can get in your first session"
|
||||
greeting: "Hello"
|
||||
|
|
@ -113,7 +110,6 @@ en:
|
|||
If you have any trouble or feel confused about gear setup, 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"
|
||||
|
||||
gear_setup_reminder2:
|
||||
subject: "Set up your gear in JamKazam now to connect with other musicians"
|
||||
greeting: "Hello"
|
||||
|
|
@ -127,7 +123,6 @@ en:
|
|||
If you have any trouble or feel confused about gear setup, 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"
|
||||
|
||||
gear_setup_reminder3:
|
||||
subject: Don’t waste your free 30-day premium gold plan - set up your gear now!
|
||||
greeting: "Hello"
|
||||
|
|
@ -143,6 +138,174 @@ en:
|
|||
If you have any trouble or feel confused about gear setup, 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"
|
||||
test_gear_reminder1:
|
||||
subject: "Test your gear in a private JamKazam session to get ready to go!"
|
||||
greeting: "Hello"
|
||||
paragraph1: |
|
||||
Now that you have set up your gear, we recommend that you get into a private session alone to test that your gear is working properly and to get familiar with some of the key features you’ll want to use when in a session with other musicians.
|
||||
paragraph2: |
|
||||
To get into a private session on your own, we recommend you connect your computer to your home router using an Ethernet cable (just to get into the practice of doing this), and then follow the instructions <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000122469">in this help article</a> to quick start a private session.
|
||||
paragraph3: |
|
||||
Once you’re in the session, we recommend you explore the following features:
|
||||
<ul>
|
||||
<li>
|
||||
Use the instructions <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000505535">in this help article</a> to set the input levels for your instrument(s) and/or voice to a healthy spot. This is an important step to get good audio, both for you and for others in your sessions.
|
||||
</li>
|
||||
<li>
|
||||
Use the instructions <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124834">in this help article</a> to adjust your personal session mix. Since you’re alone in a test session right now, you won’t be able to adjust the volume of other musicians on your session, but you’ll learn how to do it when you’re in a session with others. This feature is critical, as it lets each musician in a session dial in a mix that sounds good to them, without affecting what other musicians hear
|
||||
</li>
|
||||
<li>
|
||||
If you plan to use video in sessions, in addition to audio, use the instructions <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124837">in this help article</a> to test that your webcam is working well with the JamKazam app.
|
||||
</li>
|
||||
<li>
|
||||
You can also review help articles on how to <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124835">use backing tracks</a> in sessions, <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124836">use a metronome</a> in sessions, and <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124840">use our diagnostics</a> to troubleshoot problems in sessions.
|
||||
</li>
|
||||
<li>
|
||||
When you’re doing exploring, learning, and getting comfortable, you can <a href="https://jamkazam.freshdesk.com/support/solutions/articles/66000124841">leave your private test session</a>.
|
||||
</li>
|
||||
</ul>
|
||||
paragraph4: |
|
||||
If you still feel like you want to get more comfortable in online sessions before starting to jump in with others, we offer a weekly office hours JamKazam session every Tuesday 7-8pm US Central Time. You can join this 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 you’re 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.
|
||||
paragraph5: |
|
||||
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"
|
||||
test_gear_reminder2:
|
||||
subject: "Test your gear and learn how to use key features in a private JamKazam"
|
||||
greeting: "Hello"
|
||||
paragraph1: "Before hopping into JamKazam sessions with other musicians, we recommend you get into a private session alone to test that your gear is working properly and to get familiar with some of the key features you’ll want to use when in a session with other musicians."
|
||||
paragraph2: |
|
||||
To get into a private session on your own, start up the JamKazam app, click the big orange Create Session tile, and then click the Quick Start Private button. This will drop you into a private session alone. Once you’re in the session, we recommend you read/review the help topics <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000110433">on this help page</a> to familiarize yourself with the features explained in these help articles, as you’ll use most of these features.
|
||||
paragraph3: |
|
||||
After doing this, if you still feel like you want to get more comfortable in online sessions before starting to jump in with others, we offer a weekly office hours JamKazam session every Tuesday 7-8pm US Central Time. You can join this 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 you’re 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.
|
||||
paragraph4: |
|
||||
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"
|
||||
test_gear_reminder3:
|
||||
subject: "Don’t waste your free 30-day premium gold plan – get in a test session 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: |
|
||||
Don’t waste your 30-day window! Test your gear in a private JamKazam session now, so that you’re ready to join sessions and play with other musicians.
|
||||
paragraph3: |
|
||||
To get into a private session on your own, start up the JamKazam app, click the big orange Create Session tile, and then click the Quick Start Private button. This will drop you into a private session alone. Once you’re in the session, we recommend you read/review the help topics <a href="https://jamkazam.freshdesk.com/support/solutions/folders/66000110433">on this help page</a> to familiarize yourself with the features explained in these help articles, as you’ll use most of these features.
|
||||
paragraph4: |
|
||||
After doing this, if you still feel like you want to get more comfortable in online sessions before starting to jump in with others, we offer a weekly office hours JamKazam session every Tuesday 7-8pm US Central Time. You can join this 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 you’re 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.
|
||||
paragraph5: |
|
||||
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: "It’s 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 you’re 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 you’re 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. You’ll 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 you’ll be able to play with people you already know and love.
|
||||
finding_good_matches: Finding Good Matches in the JamKazam Community
|
||||
paragraph5: |
|
||||
If you don’t 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 you’ll 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 they’d 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 you’re 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 you’re 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 you’re looking for, and what skill level (e.g. beginner, intermediate, advanced) you’re looking for.
|
||||
paragraph8: |
|
||||
As a final suggestion, don’t be shy! If you’re worried you won’t 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 you’ll find it here 99% of the time (there’s always that one guy, right?). Reach out and make new musical friends. You’ll 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, you’re 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! There’s 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: |
|
||||
Don’t 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. You’ll 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, don’t 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: "Don’t waste your free 30-day premium gold plan–get 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: |
|
||||
Don’t waste your 30-day window! Get connected to other musicians who want to play the kinds of music you’re 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, don’t 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 you’ve enjoyed your 30-day free gold trial with JamKazam. If you’ve used it to play music in online sessions with others, below is a summary of your options to keep playing. If you haven’t finished setting up your gear or haven’t 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 you’re 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 what’s right for you.
|
||||
</li>
|
||||
</ul><br />
|
||||
paragraph2: |
|
||||
If you’d 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: "Don’t 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 what’s right for you.
|
||||
</li>
|
||||
</ul><br />
|
||||
paragraph2: |
|
||||
If you’d 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 haven’t finished setting up your gear or haven’t 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 you’re 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 what’s right for you.
|
||||
</li>
|
||||
</ul>
|
||||
paragraph2: |
|
||||
If you’d 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 haven’t finished setting up your gear or haven’t played in sessions with others yet, please send us an email at support@jamkazam.com to let us know where you’re 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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
35911
web/dev_failures.txt
35911
web/dev_failures.txt
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -52,6 +52,8 @@ class UserManager < BaseManager
|
|||
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]
|
||||
|
||||
recaptcha_failed = false
|
||||
unless options[:skip_recaptcha] # allow callers to opt-of recaptcha
|
||||
|
|
@ -114,7 +116,10 @@ class UserManager < BaseManager
|
|||
license_start: license_start,
|
||||
license_end: license_end,
|
||||
import_source: import_source,
|
||||
desired_plan_code: desired_plan_code)
|
||||
import_source: import_source,
|
||||
desired_plan_code: desired_plan_code,
|
||||
facebook_click_id: facebook_click_id,
|
||||
facebook_browser_id: facebook_browser_id)
|
||||
user
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,12 @@ describe "User API", :type => :api do
|
|||
return last_response
|
||||
end
|
||||
|
||||
def begin_update_email_alt(authenticated_user, update_email, validation_password, login = true)
|
||||
login(authenticated_user.email, authenticated_user.password, 200, true) if login
|
||||
post "/api/users/#{authenticated_user.id}/update_email_alt.json", { :update_email => update_email, :current_password => validation_password }.to_json, "CONTENT_TYPE" => 'application/json'
|
||||
return last_response
|
||||
end
|
||||
|
||||
def finalize_update_email(update_email_token)
|
||||
post "/api/users/update_email/#{update_email_token}.json", {}.to_json, "CONTENT_TYPE" => 'application/json'
|
||||
return last_response
|
||||
|
|
@ -907,7 +913,7 @@ describe "User API", :type => :api do
|
|||
end
|
||||
|
||||
########## UPDATE EMAIL ########
|
||||
describe "update email" do
|
||||
describe "update email", focus: true do
|
||||
describe "begin update email" do
|
||||
it "success" do
|
||||
last_response = begin_update_email(user, "not_taken_test@jamkazam.com", user.password)
|
||||
|
|
@ -933,6 +939,12 @@ describe "User API", :type => :api do
|
|||
last_response.status.should == 403
|
||||
UserMailer.deliveries.length.should == 0
|
||||
end
|
||||
|
||||
it "success alt" do
|
||||
last_response = begin_update_email_alt(user, "not_taken_test_alt@jamkazam.com", user.password)
|
||||
last_response.status.should == 200
|
||||
UserMailer.deliveries.length.should == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "finalize update email" do
|
||||
|
|
|
|||
|
|
@ -180,6 +180,22 @@ def web_config
|
|||
def max_invites_to_receiver_per_day
|
||||
1000 # a low number only hinders test; the ruby rspec test have a lower value and test this feature
|
||||
end
|
||||
|
||||
def facebook_pixel_id
|
||||
'1234567890'
|
||||
end
|
||||
|
||||
def facebook_ad_source
|
||||
'meta'
|
||||
end
|
||||
|
||||
def facebook_access_token
|
||||
'dummy_token'
|
||||
end
|
||||
|
||||
def facebook_conversion_api_tls
|
||||
true
|
||||
end
|
||||
end
|
||||
klass.new
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue