mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-02-17 08:50:58 -05:00
Update subscribe modal with parent help button
This commit is contained in:
parent
35c04974dd
commit
c842f45786
10 changed files with 350 additions and 143 deletions
BIN
app/assets/images/pages/play/modal/subscribe-heroes.png
Normal file
BIN
app/assets/images/pages/play/modal/subscribe-heroes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
|
@ -381,6 +381,15 @@
|
||||||
recovered: "Previous gems purchase recovered. Please refresh the page."
|
recovered: "Previous gems purchase recovered. Please refresh the page."
|
||||||
|
|
||||||
subscribe:
|
subscribe:
|
||||||
|
comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"
|
||||||
|
feature1: "60+ basic levels across 4 worlds"
|
||||||
|
feature2: "7 powerful <strong>new heroes</strong> with unique skills!"
|
||||||
|
feature3: "30+ bonus levels"
|
||||||
|
feature4: "<strong>3500 bonus gems</strong> every month!"
|
||||||
|
feature5: " Video tutorials"
|
||||||
|
feature6: "Premium email support"
|
||||||
|
free: "Free"
|
||||||
|
month: "month"
|
||||||
subscribe_title: "Subscribe"
|
subscribe_title: "Subscribe"
|
||||||
unsubscribe: "Unsubscribe"
|
unsubscribe: "Unsubscribe"
|
||||||
confirm_unsubscribe: "Confirm Unsubscribe"
|
confirm_unsubscribe: "Confirm Unsubscribe"
|
||||||
|
@ -394,12 +403,20 @@
|
||||||
heroes: "More powerful heroes!"
|
heroes: "More powerful heroes!"
|
||||||
gems: "3500 bonus gems every month!"
|
gems: "3500 bonus gems every month!"
|
||||||
items: "Over 250 bonus items!"
|
items: "Over 250 bonus items!"
|
||||||
|
parent_button: "Ask your parent"
|
||||||
|
parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
|
||||||
|
parent_email_input_invalid: "Email address invalid."
|
||||||
|
parent_email_input_label: "Parent email address"
|
||||||
|
parent_email_input_placeholder: "Enter parent email"
|
||||||
|
parent_email_send: "Send Email"
|
||||||
|
parent_email_sent: "Email sent!"
|
||||||
|
parent_email_title: "What's your parent's email?"
|
||||||
parents: "For Parents"
|
parents: "For Parents"
|
||||||
parents_title: "Your child will learn to code."
|
parents_title: "Your child will learn to code."
|
||||||
parents_blurb1: "With CodeCombat, your child learns by writing real code. They start by learning simple commands, and progress to more advanced topics."
|
parents_blurb1: "With CodeCombat, your child learns by writing real code. They start by learning simple commands, and progress to more advanced topics."
|
||||||
parents_blurb2: "For $9.99 USD/mo, they get new challenges every week and personal email support from professional programmers."
|
parents_blurb2: "For $9.99 USD/mo, they get new challenges every week and personal email support from professional programmers."
|
||||||
parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe."
|
parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe."
|
||||||
subscribe_button: "Subscribe Now"
|
subscribe_button: "Subscribe"
|
||||||
stripe_description: "Monthly Subscription"
|
stripe_description: "Monthly Subscription"
|
||||||
subscription_required_to_play: "You'll need a subscription to play this level."
|
subscription_required_to_play: "You'll need a subscription to play this level."
|
||||||
unlock_help_videos: "Subscribe to unlock all video tutorials."
|
unlock_help_videos: "Subscribe to unlock all video tutorials."
|
||||||
|
|
|
@ -85,6 +85,12 @@ _.extend UserSchema.properties,
|
||||||
recruitNotes: {$ref: '#/definitions/emailSubscription'}
|
recruitNotes: {$ref: '#/definitions/emailSubscription'}
|
||||||
employerNotes: {$ref: '#/definitions/emailSubscription'}
|
employerNotes: {$ref: '#/definitions/emailSubscription'}
|
||||||
|
|
||||||
|
oneTimes: c.array {title: 'One-time emails'},
|
||||||
|
c.object {title: 'One-time email', required: ['type', 'targetEmail']},
|
||||||
|
type: c.shortString() # E.g 'subscribe modal parent'
|
||||||
|
targetEmail: c.shortString()
|
||||||
|
sent: c.date() # Set when sent
|
||||||
|
|
||||||
# server controlled
|
# server controlled
|
||||||
permissions: c.array {}, c.shortString()
|
permissions: c.array {}, c.shortString()
|
||||||
dateCreated: c.date({title: 'Date Joined'})
|
dateCreated: c.date({title: 'Date Joined'})
|
||||||
|
@ -272,7 +278,7 @@ _.extend UserSchema.properties,
|
||||||
purchased: c.RewardSchema 'purchased with gems or money'
|
purchased: c.RewardSchema 'purchased with gems or money'
|
||||||
spent: {type: 'number'}
|
spent: {type: 'number'}
|
||||||
stripeCustomerID: { type: 'string' } # TODO: Migrate away from this property
|
stripeCustomerID: { type: 'string' } # TODO: Migrate away from this property
|
||||||
|
|
||||||
stripe: c.object {}, {
|
stripe: c.object {}, {
|
||||||
customerID: { type: 'string' }
|
customerID: { type: 'string' }
|
||||||
planID: { enum: ['basic'] }
|
planID: { enum: ['basic'] }
|
||||||
|
|
|
@ -51,36 +51,11 @@
|
||||||
&:hover
|
&:hover
|
||||||
color: yellow
|
color: yellow
|
||||||
|
|
||||||
|
//- Popovers
|
||||||
//- Selling points
|
|
||||||
|
|
||||||
#selling-points
|
|
||||||
position: absolute
|
|
||||||
left: 65px
|
|
||||||
top: 335px
|
|
||||||
width: 650px
|
|
||||||
font-weight: bold
|
|
||||||
line-height: 18px
|
|
||||||
color: black
|
|
||||||
font-family: $headings-font-family
|
|
||||||
font-size: 18px
|
|
||||||
|
|
||||||
.point
|
|
||||||
width: 150px
|
|
||||||
overflow: none
|
|
||||||
float: left
|
|
||||||
text-align: center
|
|
||||||
margin-right: 10px
|
|
||||||
|
|
||||||
#parents-info
|
|
||||||
position: absolute
|
|
||||||
right: 7px
|
|
||||||
top: 56px
|
|
||||||
text-decoration: underline
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
.popover
|
.popover
|
||||||
z-index: 1050
|
z-index: 1050
|
||||||
|
min-width: 400px
|
||||||
|
|
||||||
h3
|
h3
|
||||||
background: transparent
|
background: transparent
|
||||||
|
@ -88,13 +63,67 @@
|
||||||
font-size: 30px
|
font-size: 30px
|
||||||
color: black
|
color: black
|
||||||
|
|
||||||
|
//- Sales image
|
||||||
|
|
||||||
|
.subscribe-image
|
||||||
|
position: absolute
|
||||||
|
top: 114px
|
||||||
|
right: 65px
|
||||||
|
|
||||||
|
//- Feature comparison table
|
||||||
|
|
||||||
|
.comparison-blurb
|
||||||
|
position: absolute
|
||||||
|
left: 10%
|
||||||
|
top: 132px
|
||||||
|
width: 450px
|
||||||
|
background: rgba(0, 0, 0, 0.0)
|
||||||
|
font-weight: normal
|
||||||
|
line-height: 18px
|
||||||
|
color: black
|
||||||
|
font-family: $headings-font-family
|
||||||
|
font-size: 18px
|
||||||
|
|
||||||
|
.comparison-table
|
||||||
|
position: absolute
|
||||||
|
left: 10%
|
||||||
|
top: 160px
|
||||||
|
width: 450px
|
||||||
|
background: rgba(0, 0, 0, 0.0)
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th
|
||||||
|
font-size: 24px
|
||||||
|
font-variant: small-caps
|
||||||
|
font-family: "Open Sans Condensed", "Helvetica Neue", Helvetica, Arial, sans-serif
|
||||||
|
font-weight: 700
|
||||||
|
line-height: 1.1
|
||||||
|
color: #317EAC
|
||||||
|
tbody
|
||||||
|
font-size: 14px
|
||||||
|
.center-ok
|
||||||
|
text-align: center
|
||||||
|
|
||||||
|
//- Parent info popover link
|
||||||
|
|
||||||
|
#parents-info
|
||||||
|
position: absolute
|
||||||
|
left: 38px
|
||||||
|
top: 389px
|
||||||
|
text-decoration: underline
|
||||||
|
cursor: pointer
|
||||||
|
font-weight: bold
|
||||||
|
line-height: 18px
|
||||||
|
color: black
|
||||||
|
font-family: $headings-font-family
|
||||||
|
font-size: 18px
|
||||||
|
|
||||||
//- Purchase button
|
//- Purchase button
|
||||||
|
|
||||||
.purchase-button
|
.purchase-button
|
||||||
position: absolute
|
position: absolute
|
||||||
left: 73px
|
right: 24px
|
||||||
width: 600px
|
width: 400px
|
||||||
height: 70px
|
height: 70px
|
||||||
top: 430px
|
top: 430px
|
||||||
font-size: 32px
|
font-size: 32px
|
||||||
|
@ -116,6 +145,28 @@
|
||||||
padding: 2px 0 0 2px
|
padding: 2px 0 0 2px
|
||||||
color: white
|
color: white
|
||||||
|
|
||||||
|
//- Parent button
|
||||||
|
//- TODO: Add hover and active effects
|
||||||
|
|
||||||
|
.parent-button
|
||||||
|
position: absolute
|
||||||
|
left: 24px
|
||||||
|
width: 250px
|
||||||
|
height: 70px
|
||||||
|
top: 430px
|
||||||
|
font-size: 28px
|
||||||
|
line-height: 38px
|
||||||
|
border-style: solid
|
||||||
|
border-image: url(/images/common/button-background-warning-disabled.png) 14 20 20 20 fill round
|
||||||
|
border-width: 14px 20px 20px 20px
|
||||||
|
color: darken(white, 5%)
|
||||||
|
|
||||||
|
#email-parent-form
|
||||||
|
.email_invalid
|
||||||
|
color: red
|
||||||
|
display: none
|
||||||
|
#email-parent-complete
|
||||||
|
display: none
|
||||||
|
|
||||||
//- Errors
|
//- Errors
|
||||||
|
|
||||||
|
|
|
@ -7,27 +7,67 @@
|
||||||
#retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
|
#retrying-alert.alert.alert-danger(data-i18n="buy_gems.retrying")
|
||||||
|
|
||||||
else
|
else
|
||||||
img(src="/images/pages/play/modal/subscribe-background.png")#subscribe-background
|
img#subscribe-background(src="/images/pages/play/modal/subscribe-background-blank.png")
|
||||||
|
img.subscribe-image(src="/images/pages/play/modal/subscribe-heroes.png")
|
||||||
|
|
||||||
h1(data-i18n="subscribe.subscribe_title") Subscribe
|
h1(data-i18n="subscribe.subscribe_title") Subscribe
|
||||||
|
|
||||||
div#close-modal
|
div#close-modal
|
||||||
span.glyphicon.glyphicon-remove
|
span.glyphicon.glyphicon-remove
|
||||||
|
|
||||||
#selling-points
|
div.comparison-blurb(data-i18n="subscribe.comparison_blurb")
|
||||||
#point-levels.point
|
table.table.table-condensed.table-bordered.comparison-table
|
||||||
.blurb(data-i18n="subscribe.levels")
|
thead
|
||||||
#point-heroes.point
|
tr
|
||||||
.blurb(data-i18n="subscribe.heroes")
|
th
|
||||||
#point-gems.point
|
th(data-i18n="subscribe.free")
|
||||||
.blurb(data-i18n="subscribe.gems")
|
th
|
||||||
#point-items.point
|
//- TODO: find a better way to localize '$9.99/month'
|
||||||
.blurb(data-i18n="subscribe.items")
|
span $#{price}/
|
||||||
|
span(data-i18n="subscribe.month")
|
||||||
#parents-info(data-i18n="subscribe.parents")
|
tbody
|
||||||
|
tr
|
||||||
|
td.feature-description
|
||||||
|
span(data-i18n="subscribe.feature1")
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
tr
|
||||||
|
td.feature-description
|
||||||
|
span(data-i18n="[html]subscribe.feature2")
|
||||||
|
td
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
tr
|
||||||
|
td.feature-description
|
||||||
|
span(data-i18n="subscribe.feature3")
|
||||||
|
td
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
tr
|
||||||
|
td.feature-description
|
||||||
|
span(data-i18n="[html]subscribe.feature4")
|
||||||
|
td
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
tr
|
||||||
|
td.feature-description
|
||||||
|
span(data-i18n="subscribe.feature5")
|
||||||
|
td
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
tr
|
||||||
|
td.feature-description
|
||||||
|
span(data-i18n="subscribe.feature6")
|
||||||
|
td
|
||||||
|
td.center-ok
|
||||||
|
span.glyphicon.glyphicon-ok
|
||||||
|
#parents-info(data-i18n="subscribe.parents")
|
||||||
|
|
||||||
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
|
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
|
||||||
|
button.btn.btn-lg.btn-illustrated.parent-button(data-i18n="subscribe.parent_button")
|
||||||
|
|
||||||
if state === 'declined'
|
if state === 'declined'
|
||||||
#declined-alert.alert.alert-danger.alert-dismissible
|
#declined-alert.alert.alert-danger.alert-dismissible
|
||||||
span(data-i18n="buy_gems.declined")
|
span(data-i18n="buy_gems.declined")
|
||||||
|
|
|
@ -17,8 +17,9 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
'stripe:received-token': 'onStripeReceivedToken'
|
'stripe:received-token': 'onStripeReceivedToken'
|
||||||
|
|
||||||
events:
|
events:
|
||||||
'click .purchase-button': 'onClickPurchaseButton'
|
|
||||||
'click #close-modal': 'hide'
|
'click #close-modal': 'hide'
|
||||||
|
'click #parent-send': 'onClickParentSendButton'
|
||||||
|
'click .purchase-button': 'onClickPurchaseButton'
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super(options)
|
super(options)
|
||||||
|
@ -34,6 +35,40 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
|
@setupParentButtonPopover()
|
||||||
|
@setupParentInfoPopover()
|
||||||
|
|
||||||
|
setupParentButtonPopover: ->
|
||||||
|
popoverTitle = $.i18n.t 'subscribe.parent_email_title'
|
||||||
|
popoverTitle += '<button type="button" class="close" onclick="$('.parent-button').popover('hide');">×</button>'
|
||||||
|
popoverContent = "<div id='email-parent-form'>"
|
||||||
|
popoverContent += "<p>#{$.i18n.t('subscribe.parent_email_description')}</p>"
|
||||||
|
popoverContent += "<form>"
|
||||||
|
popoverContent += " <div class='form-group'>"
|
||||||
|
popoverContent += " <label>#{$.i18n.t('subscribe.parent_email_input_label')}</label>"
|
||||||
|
popoverContent += " <input id='parent-input' type='email' class='form-control' placeholder='#{$.i18n.t('subscribe.parent_email_input_placeholder')}'/>"
|
||||||
|
popoverContent += " <div id='parent-email-validator' class='email_invalid'>#{$.i18n.t('subscribe.parent_email_input_invalid')}</div>"
|
||||||
|
popoverContent += " </div>"
|
||||||
|
popoverContent += " <button id='parent-send' type='submit' class='btn btn-default'>#{$.i18n.t('subscribe.parent_email_send')}</button>"
|
||||||
|
popoverContent += "</form>"
|
||||||
|
popoverContent += "</div>"
|
||||||
|
popoverContent += "<div id='email-parent-complete'>"
|
||||||
|
popoverContent += " <p>#{$.i18n.t('subscribe.parent_email_sent')}</p>"
|
||||||
|
popoverContent += " <button type='button' onclick='$('.parent-button').popover('hide');'>#{$.i18n.t('modal.close')}</button>"
|
||||||
|
popoverContent += "</div>"
|
||||||
|
|
||||||
|
@$el.find('.parent-button').popover(
|
||||||
|
animation: true
|
||||||
|
html: true
|
||||||
|
placement: 'top'
|
||||||
|
trigger: 'click'
|
||||||
|
title: popoverTitle
|
||||||
|
content: popoverContent
|
||||||
|
container: @$el
|
||||||
|
).on 'shown.bs.popover', =>
|
||||||
|
application.tracker?.trackEvent 'Subscription ask parent button click', {}
|
||||||
|
|
||||||
|
setupParentInfoPopover: ->
|
||||||
popoverTitle = $.i18n.t 'subscribe.parents_title'
|
popoverTitle = $.i18n.t 'subscribe.parents_title'
|
||||||
popoverContent = "<p>" + $.i18n.t('subscribe.parents_blurb1') + "</p>"
|
popoverContent = "<p>" + $.i18n.t('subscribe.parents_blurb1') + "</p>"
|
||||||
popoverContent += "<p>" + $.i18n.t('subscribe.parents_blurb2') + "</p>"
|
popoverContent += "<p>" + $.i18n.t('subscribe.parents_blurb2') + "</p>"
|
||||||
|
@ -50,6 +85,26 @@ module.exports = class SubscribeModal extends ModalView
|
||||||
).on 'shown.bs.popover', =>
|
).on 'shown.bs.popover', =>
|
||||||
application.tracker?.trackEvent 'Subscription parent hover', {}
|
application.tracker?.trackEvent 'Subscription parent hover', {}
|
||||||
|
|
||||||
|
onClickParentSendButton: (e) ->
|
||||||
|
# TODO: Popover sometimes dismisses immediately after send
|
||||||
|
|
||||||
|
email = $('#parent-input').val()
|
||||||
|
unless /[\w\.]+@\w+\.\w+/.test email
|
||||||
|
$('#parent-input').parent().addClass('has-error')
|
||||||
|
$('#parent-email-validator').show()
|
||||||
|
return false
|
||||||
|
|
||||||
|
request = @supermodel.addRequestResource 'send_one_time_email', {
|
||||||
|
url: '/db/user/-/send_one_time_email'
|
||||||
|
data: {email: email, type: 'subscribe modal parent'}
|
||||||
|
method: 'POST'
|
||||||
|
}, 0
|
||||||
|
request.load()
|
||||||
|
|
||||||
|
$('#email-parent-form').hide()
|
||||||
|
$('#email-parent-complete').show()
|
||||||
|
false
|
||||||
|
|
||||||
onClickPurchaseButton: (e) ->
|
onClickPurchaseButton: (e) ->
|
||||||
@playSound 'menu-button-click'
|
@playSound 'menu-button-click'
|
||||||
return @openModalView new AuthModal() if me.get('anonymous')
|
return @openModalView new AuthModal() if me.get('anonymous')
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
log = require 'winston'
|
||||||
mongoose = require 'mongoose'
|
mongoose = require 'mongoose'
|
||||||
plugins = require '../plugins/plugins'
|
plugins = require '../plugins/plugins'
|
||||||
|
utils = require '../lib/utils'
|
||||||
|
|
||||||
AnalyticsLogEventSchema = new mongoose.Schema({
|
AnalyticsLogEventSchema = new mongoose.Schema({
|
||||||
u: mongoose.Schema.Types.ObjectId
|
u: mongoose.Schema.Types.ObjectId
|
||||||
|
@ -14,4 +16,102 @@ AnalyticsLogEventSchema = new mongoose.Schema({
|
||||||
|
|
||||||
AnalyticsLogEventSchema.index({event: 1, _id: 1})
|
AnalyticsLogEventSchema.index({event: 1, _id: 1})
|
||||||
|
|
||||||
|
AnalyticsLogEventSchema.statics.logEvent = (user, event, properties) ->
|
||||||
|
unless user?
|
||||||
|
log.warn 'No user given to analytics logEvent.'
|
||||||
|
return
|
||||||
|
|
||||||
|
saveDoc = (eventID, slimProperties) ->
|
||||||
|
doc = new AnalyticsLogEvent
|
||||||
|
u: user
|
||||||
|
e: eventID
|
||||||
|
p: slimProperties
|
||||||
|
# TODO: Remove these legacy properties after we stop querying for them (probably 30 days, ~2/16/15)
|
||||||
|
user: user
|
||||||
|
event: event
|
||||||
|
properties: properties
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
utils.getAnalyticsStringID event, (eventID) ->
|
||||||
|
if eventID > 0
|
||||||
|
# TODO: properties slimming is pretty ugly
|
||||||
|
slimProperties = _.cloneDeep properties
|
||||||
|
if event in ['Clicked Level', 'Show problem alert', 'Started Level', 'Saw Victory', 'Problem alert help clicked', 'Spell palette help clicked']
|
||||||
|
delete slimProperties.level if event is 'Saw Victory'
|
||||||
|
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||||
|
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||||
|
if slimProperties.levelID?
|
||||||
|
# levelID: string => l: string ID
|
||||||
|
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
||||||
|
if levelStringID > 0
|
||||||
|
delete slimProperties.levelID
|
||||||
|
slimProperties.l = levelStringID
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
return
|
||||||
|
else if event in ['Script Started', 'Script Ended']
|
||||||
|
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||||
|
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||||
|
if slimProperties.levelID? and slimProperties.label?
|
||||||
|
# levelID: string => l: string ID
|
||||||
|
# label: string => lb: string ID
|
||||||
|
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
||||||
|
if levelStringID > 0
|
||||||
|
delete slimProperties.levelID
|
||||||
|
slimProperties.l = levelStringID
|
||||||
|
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
||||||
|
if labelStringID > 0
|
||||||
|
delete slimProperties.label
|
||||||
|
slimProperties.lb = labelStringID
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
return
|
||||||
|
else if event is 'Heard Sprite'
|
||||||
|
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||||
|
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||||
|
if slimProperties.message?
|
||||||
|
# message: string => m: string ID
|
||||||
|
utils.getAnalyticsStringID slimProperties.message, (messageStringID) ->
|
||||||
|
if messageStringID > 0
|
||||||
|
delete slimProperties.message
|
||||||
|
slimProperties.m = messageStringID
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
return
|
||||||
|
else if event in ['Start help video', 'Finish help video']
|
||||||
|
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
||||||
|
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
||||||
|
if slimProperties.level and slimProperties.style?
|
||||||
|
# level: string => l: string ID
|
||||||
|
# style: string => s: string ID
|
||||||
|
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
||||||
|
if levelStringID > 0
|
||||||
|
delete slimProperties.level
|
||||||
|
slimProperties.l = levelStringID
|
||||||
|
utils.getAnalyticsStringID slimProperties.style, (styleStringID) ->
|
||||||
|
if styleStringID > 0
|
||||||
|
delete slimProperties.style
|
||||||
|
slimProperties.s = styleStringID
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
return
|
||||||
|
else if event is 'Show subscription modal'
|
||||||
|
delete properties.category
|
||||||
|
delete slimProperties.category
|
||||||
|
if slimProperties.label?
|
||||||
|
# label: string => lb: string ID
|
||||||
|
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
||||||
|
if labelStringID > 0
|
||||||
|
delete slimProperties.label
|
||||||
|
slimProperties.lb = labelStringID
|
||||||
|
if slimProperties.level?
|
||||||
|
# level: string => l: string ID
|
||||||
|
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
||||||
|
if levelStringID > 0
|
||||||
|
delete slimProperties.level
|
||||||
|
slimProperties.l = levelStringID
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
return
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
return
|
||||||
|
saveDoc eventID, slimProperties
|
||||||
|
else
|
||||||
|
log.warn "Unable to get analytics string ID for " + event
|
||||||
|
|
||||||
module.exports = AnalyticsLogEvent = mongoose.model('analytics.log.event', AnalyticsLogEventSchema)
|
module.exports = AnalyticsLogEvent = mongoose.model('analytics.log.event', AnalyticsLogEventSchema)
|
||||||
|
|
|
@ -39,102 +39,7 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
event = req.query.event or req.body.event
|
event = req.query.event or req.body.event
|
||||||
properties = req.query.properties or req.body.properties
|
properties = req.query.properties or req.body.properties
|
||||||
@sendSuccess res # Return request immediately
|
@sendSuccess res # Return request immediately
|
||||||
unless user?
|
AnalyticsLogEvent.logEvent user, event, properties
|
||||||
log.warn 'No user given to analytics logEvent.'
|
|
||||||
return
|
|
||||||
|
|
||||||
saveDoc = (eventID, slimProperties) ->
|
|
||||||
doc = new AnalyticsLogEvent
|
|
||||||
u: user
|
|
||||||
e: eventID
|
|
||||||
p: slimProperties
|
|
||||||
# TODO: Remove these legacy properties after we stop querying for them (probably 30 days, ~2/16/15)
|
|
||||||
user: user
|
|
||||||
event: event
|
|
||||||
properties: properties
|
|
||||||
doc.save()
|
|
||||||
|
|
||||||
utils.getAnalyticsStringID event, (eventID) ->
|
|
||||||
if eventID > 0
|
|
||||||
# TODO: properties slimming is pretty ugly
|
|
||||||
slimProperties = _.cloneDeep properties
|
|
||||||
if event in ['Clicked Level', 'Show problem alert', 'Started Level', 'Saw Victory', 'Problem alert help clicked', 'Spell palette help clicked']
|
|
||||||
delete slimProperties.level if event is 'Saw Victory'
|
|
||||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
|
||||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
|
||||||
if slimProperties.levelID?
|
|
||||||
# levelID: string => l: string ID
|
|
||||||
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
|
||||||
if levelStringID > 0
|
|
||||||
delete slimProperties.levelID
|
|
||||||
slimProperties.l = levelStringID
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
return
|
|
||||||
else if event in ['Script Started', 'Script Ended']
|
|
||||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
|
||||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
|
||||||
if slimProperties.levelID? and slimProperties.label?
|
|
||||||
# levelID: string => l: string ID
|
|
||||||
# label: string => lb: string ID
|
|
||||||
utils.getAnalyticsStringID slimProperties.levelID, (levelStringID) ->
|
|
||||||
if levelStringID > 0
|
|
||||||
delete slimProperties.levelID
|
|
||||||
slimProperties.l = levelStringID
|
|
||||||
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
|
||||||
if labelStringID > 0
|
|
||||||
delete slimProperties.label
|
|
||||||
slimProperties.lb = labelStringID
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
return
|
|
||||||
else if event is 'Heard Sprite'
|
|
||||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
|
||||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
|
||||||
if slimProperties.message?
|
|
||||||
# message: string => m: string ID
|
|
||||||
utils.getAnalyticsStringID slimProperties.message, (messageStringID) ->
|
|
||||||
if messageStringID > 0
|
|
||||||
delete slimProperties.message
|
|
||||||
slimProperties.m = messageStringID
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
return
|
|
||||||
else if event in ['Start help video', 'Finish help video']
|
|
||||||
properties.ls = mongoose.Types.ObjectId properties.ls if properties.ls
|
|
||||||
slimProperties.ls = mongoose.Types.ObjectId slimProperties.ls if slimProperties.ls
|
|
||||||
if slimProperties.level and slimProperties.style?
|
|
||||||
# level: string => l: string ID
|
|
||||||
# style: string => s: string ID
|
|
||||||
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
|
||||||
if levelStringID > 0
|
|
||||||
delete slimProperties.level
|
|
||||||
slimProperties.l = levelStringID
|
|
||||||
utils.getAnalyticsStringID slimProperties.style, (styleStringID) ->
|
|
||||||
if styleStringID > 0
|
|
||||||
delete slimProperties.style
|
|
||||||
slimProperties.s = styleStringID
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
return
|
|
||||||
else if event is 'Show subscription modal'
|
|
||||||
delete properties.category
|
|
||||||
delete slimProperties.category
|
|
||||||
if slimProperties.label?
|
|
||||||
# label: string => lb: string ID
|
|
||||||
utils.getAnalyticsStringID slimProperties.label, (labelStringID) ->
|
|
||||||
if labelStringID > 0
|
|
||||||
delete slimProperties.label
|
|
||||||
slimProperties.lb = labelStringID
|
|
||||||
if slimProperties.level?
|
|
||||||
# level: string => l: string ID
|
|
||||||
utils.getAnalyticsStringID slimProperties.level, (levelStringID) ->
|
|
||||||
if levelStringID > 0
|
|
||||||
delete slimProperties.level
|
|
||||||
slimProperties.l = levelStringID
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
return
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
return
|
|
||||||
saveDoc eventID, slimProperties
|
|
||||||
else
|
|
||||||
log.warn "Unable to get analytics string ID for " + event
|
|
||||||
|
|
||||||
getLevelCompletionsBySlug: (req, res) ->
|
getLevelCompletionsBySlug: (req, res) ->
|
||||||
# Returns an array of per-day level starts and finishes
|
# Returns an array of per-day level starts and finishes
|
||||||
|
@ -204,7 +109,7 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
for level of levelDateMap
|
for level of levelDateMap
|
||||||
completions[level] = []
|
completions[level] = []
|
||||||
for created, item of levelDateMap[level]
|
for created, item of levelDateMap[level]
|
||||||
completions[level].push
|
completions[level].push
|
||||||
level: level
|
level: level
|
||||||
created: created
|
created: created
|
||||||
started: Object.keys(item.started).length
|
started: Object.keys(item.started).length
|
||||||
|
@ -382,7 +287,7 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
getUserEventData campaigns
|
getUserEventData campaigns
|
||||||
|
|
||||||
getCampaignData = () =>
|
getCampaignData = () =>
|
||||||
# Get campaign data
|
# Get campaign data
|
||||||
# Output:
|
# Output:
|
||||||
# campaigns - per-campaign dictionary of ordered levelIDs
|
# campaigns - per-campaign dictionary of ordered levelIDs
|
||||||
# campaignLevelIDs - dictionary of all campaign levelIDs
|
# campaignLevelIDs - dictionary of all campaign levelIDs
|
||||||
|
|
|
@ -10,6 +10,7 @@ module.exports.api = new sendwithusAPI swuAPIKey, debug
|
||||||
if config.unittest
|
if config.unittest
|
||||||
module.exports.api.send = ->
|
module.exports.api.send = ->
|
||||||
module.exports.templates =
|
module.exports.templates =
|
||||||
|
parent_subscribe_email: 'tem_2APERafogvwKhmcnouigud'
|
||||||
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
|
welcome_email: 'utnGaBHuSU4Hmsi7qrAypU'
|
||||||
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
|
ladder_update_email: 'JzaZxf39A4cKMxpPZUfWy4'
|
||||||
patch_created: 'tem_xhxuNosLALsizTNojBjNcL'
|
patch_created: 'tem_xhxuNosLALsizTNojBjNcL'
|
||||||
|
|
|
@ -9,6 +9,7 @@ errors = require '../commons/errors'
|
||||||
async = require 'async'
|
async = require 'async'
|
||||||
log = require 'winston'
|
log = require 'winston'
|
||||||
moment = require 'moment'
|
moment = require 'moment'
|
||||||
|
AnalyticsLogEvent = require '../analytics/AnalyticsLogEvent'
|
||||||
LevelSession = require '../levels/sessions/LevelSession'
|
LevelSession = require '../levels/sessions/LevelSession'
|
||||||
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
LevelSessionHandler = require '../levels/sessions/level_session_handler'
|
||||||
SubscriptionHandler = require '../payments/subscription_handler'
|
SubscriptionHandler = require '../payments/subscription_handler'
|
||||||
|
@ -17,6 +18,7 @@ EarnedAchievement = require '../achievements/EarnedAchievement'
|
||||||
UserRemark = require './remarks/UserRemark'
|
UserRemark = require './remarks/UserRemark'
|
||||||
{isID} = require '../lib/utils'
|
{isID} = require '../lib/utils'
|
||||||
hipchat = require '../hipchat'
|
hipchat = require '../hipchat'
|
||||||
|
sendwithus = require '../sendwithus'
|
||||||
|
|
||||||
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
|
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
|
||||||
candidateProperties = [
|
candidateProperties = [
|
||||||
|
@ -232,6 +234,7 @@ UserHandler = class UserHandler extends Handler
|
||||||
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
||||||
return @searchForUser(req, res) if args[1] is 'admin_search'
|
return @searchForUser(req, res) if args[1] is 'admin_search'
|
||||||
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
||||||
|
return @sendOneTimeEmail(req, res, args[0]) if args[1] is 'send_one_time_email'
|
||||||
return @sendNotFoundError(res)
|
return @sendNotFoundError(res)
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
|
|
||||||
|
@ -244,6 +247,35 @@ UserHandler = class UserHandler extends Handler
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
@sendSuccess(res, JSON.stringify(customer, null, '\t'))
|
@sendSuccess(res, JSON.stringify(customer, null, '\t'))
|
||||||
|
|
||||||
|
sendOneTimeEmail: (req, res) ->
|
||||||
|
# TODO: should this API be somewhere else?
|
||||||
|
# TODO: hipchat tower success message shows up as a misleading PaperTrail error message
|
||||||
|
return @sendForbiddenError(res) unless req.user
|
||||||
|
email = req.query.email or req.body.email
|
||||||
|
type = req.query.type or req.body.type
|
||||||
|
return @sendBadInputError res, 'No email given.' unless email?
|
||||||
|
return @sendBadInputError res, 'No type given.' unless type?
|
||||||
|
|
||||||
|
return @sendBadInputError res, "Unknown one-time email type #{type}" unless type is 'subscribe modal parent'
|
||||||
|
|
||||||
|
emailParams =
|
||||||
|
email_id: sendwithus.templates.parent_subscribe_email
|
||||||
|
recipient:
|
||||||
|
address: email
|
||||||
|
email_data:
|
||||||
|
name: req.user.get('name') or ''
|
||||||
|
sendwithus.api.send emailParams, (err, result) =>
|
||||||
|
if err
|
||||||
|
log.error "sendwithus one-time email error: #{err}, result: #{result}"
|
||||||
|
return @sendError res, 500, 'send mail failed.'
|
||||||
|
req.user.update {$push: {"emails.oneTimes": {type: type, email: email, sent: new Date()}}}, (err) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
req.user.save (err) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, {result: 'success'})
|
||||||
|
hipchat.sendTowerHipChatMessage "#{req.user.get('name') or req.user.get('email')} just submitted subscribe modal parent email #{email}."
|
||||||
|
AnalyticsLogEvent.logEvent req.user, 'Sent one time email', email: email, type: type
|
||||||
|
|
||||||
agreeToCLA: (req, res) ->
|
agreeToCLA: (req, res) ->
|
||||||
return @sendForbiddenError(res) unless req.user
|
return @sendForbiddenError(res) unless req.user
|
||||||
doc =
|
doc =
|
||||||
|
|
Loading…
Reference in a new issue