Merge branch 'master' into production
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-brown-active.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-brown-disabled.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-brown-pressed.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/assets/images/common/button-background-fb-active-border.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-fb-active.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-fb-disabled.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/assets/images/common/button-background-fb-pressed-border.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-fb-pressed.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-gplus-active.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-gplus-disabled.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/common/button-background-gplus-pressed.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/assets/images/level/loading_left_wing_1366.jpg
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
app/assets/images/level/loading_left_wing_1920.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
app/assets/images/level/loading_right_wing_1366.jpg
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
app/assets/images/level/loading_right_wing_1920.jpg
Normal file
After Width: | Height: | Size: 167 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB |
BIN
app/assets/images/pages/play/map_dungeon_1366.jpg
Normal file
After Width: | Height: | Size: 346 KiB |
BIN
app/assets/images/pages/play/map_dungeon_1920.jpg
Normal file
After Width: | Height: | Size: 614 KiB |
BIN
app/assets/images/pages/play/map_forest_1366.jpg
Normal file
After Width: | Height: | Size: 441 KiB |
BIN
app/assets/images/pages/play/map_forest_1920.jpg
Normal file
After Width: | Height: | Size: 763 KiB |
|
@ -346,7 +346,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
|
|||
parents: "Für Eltern"
|
||||
parents_title: "Dein Kind lernt zu programmieren."
|
||||
parents_blurb1: "Mit CodeCombat, lernt dein Kind richtige Programme zu schreiben. Es fängt mit einfachen Befehlen an, und schreitet ganz unmerklich zu schwierigeren Themen fort."
|
||||
parents_blurb2: "Für 9.99 im Monat, bekommt es jede Woche neue Herausforderungen sowie persönlichen email support von professionellen Programmierern."
|
||||
parents_blurb2: "Für 9.99 im Monat, bekommt es jede Woche neue Herausforderungen sowie persönlichen email support von professionellen Programmierern." # Please update based on new wording from en.coffee
|
||||
parents_blurb3: "Kein Risiko: 100% Geld zurück Garantie, und 1-Klick Abokündigung."
|
||||
subscribe_button: "Abonniere jetzt"
|
||||
stripe_description: "Monatsabo"
|
||||
|
@ -641,7 +641,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
|
|||
character_classes_title: "Charakter Klassen"
|
||||
introduction_desc_intro: "Wir haben hohe Erwartungen für CodeCombat."
|
||||
introduction_desc_pref: "Wir wollen ein Ort sein, an dem sich Programmierer aller coleur treffen, um gemeinsam zu spielen und zu lernen. Wo sie andere in die wunderbare Welt des Programmierens einführen, und dabei die besten Aspekte der Programmierer-Community verkörpern. Wir können und wollen dies nicht alleine tun. Projekte wie GitHub, Stack Overflow and Linux leben von den großartigen Leuten die sie bauen und nutzen. Darum ist, "
|
||||
introduction_desc_github_url: "CodeCombat komplett OpenSource"
|
||||
introduction_desc_github_url: "CodeCombat ist komplett OpenSource"
|
||||
introduction_desc_suf: ". Wir wollen es dir so einfach und vielfältig wie irgend möglich machen, dich an diesem Projekt zu beteiligen. Damit es genauso zu deinem Projekt wird, wie zu unserem."
|
||||
introduction_desc_ending: "Wir hoffen du nimmst an unserer Party teil!"
|
||||
introduction_desc_signature: "- Nick, George, Scott, Michael, Jeremy and Matt"
|
||||
|
|
|
@ -227,12 +227,6 @@
|
|||
victory_rate_the_level: "Rate the level: " # Only in old-style levels.
|
||||
victory_return_to_ladder: "Return to Ladder"
|
||||
victory_play_continue: "Continue"
|
||||
victory_play_skip: "Skip Ahead"
|
||||
victory_play_next_level: "Play Next Level"
|
||||
victory_play_more_practice: "More Practice"
|
||||
victory_play_too_easy: "Too Easy"
|
||||
victory_play_just_right: "Just Right"
|
||||
victory_play_too_hard: "Too Hard"
|
||||
victory_saving_progress: "Saving Progress"
|
||||
victory_go_home: "Go Home" # Only in old-style levels.
|
||||
victory_review: "Tell us more!" # Only in old-style levels.
|
||||
|
@ -339,7 +333,7 @@
|
|||
|
||||
subscribe:
|
||||
subscribe_title: "Subscribe"
|
||||
levels: "Unlock 25 levels! With 5 new ones every week!"
|
||||
levels: "Unlock 17 extra levels! With 5 new ones every week!"
|
||||
heroes: "More powerful heroes!"
|
||||
gems: "3500 bonus gems every month!"
|
||||
items: "Over 250 bonus items!"
|
||||
|
@ -925,7 +919,7 @@
|
|||
email_settings_url: "your email settings"
|
||||
email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time."
|
||||
cost_title: "Cost"
|
||||
cost_description: "CodeCombat is free to play in the dungeon campaign, with a $9.99 USD/mo subscription for access to later campaigns and 3500 bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
|
||||
cost_description: "CodeCombat is free to play for all of its core levels, with a $9.99 USD/mo subscription for access to extra level branches and 3500 bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
|
||||
copyrights_title: "Copyrights and Licenses"
|
||||
contributor_title: "Contributor License Agreement"
|
||||
contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our"
|
||||
|
|
|
@ -346,7 +346,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
|
|||
parents: "Para Educadores"
|
||||
parents_title: "O teu educando vai aprender a programar."
|
||||
parents_blurb1: "Com o CodeCombat, o teu educando aprende ao escrever código real. Começa por aprender comandos simples e progride para tópicos mais avançados."
|
||||
parents_blurb2: "Por $9.99 USD/mês, recebe novos desafios todas as semanas e suporte pessoal, via e-mail, de programadores profissionais."
|
||||
parents_blurb2: "Por $9.99 USD/mês, recebe novos desafios todas as semanas e suporte pessoal, via e-mail, de programadores profissionais." # Please update based on new wording from en.coffee
|
||||
parents_blurb3: "Sem Risco: 100% de garantia de devolução do dinheiro, com anulação fácil de 1 clique."
|
||||
subscribe_button: "Subscrever Agora"
|
||||
stripe_description: "Subscrição Mensal"
|
||||
|
|
|
@ -346,7 +346,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
|
|||
parents: "Для Родителей"
|
||||
parents_title: "Ваш ребенок научиться программировать."
|
||||
parents_blurb1: "С CodeCombat ваш ребенок учится через написание реального кода. Начиная с изучения простых команд, продолжая более продвинутыми темами."
|
||||
parents_blurb2: "За $9.99/месяц они получат новые испытания каждую неделю и персональную поддержку профессиональных программистов через электронную почту."
|
||||
parents_blurb2: "За $9.99/месяц они получат новые испытания каждую неделю и персональную поддержку профессиональных программистов через электронную почту." # Please update based on new wording from en.coffee
|
||||
parents_blurb3: "Без риска: 100% гарантия возрата денег, возможность отписаться в 1 клик."
|
||||
subscribe_button: "Подпишись сейчас"
|
||||
stripe_description: "Месячная подписка"
|
||||
|
@ -925,7 +925,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
|
|||
email_settings_url: "ваши email настройки"
|
||||
email_description_suffix: "или через ссылки в email-ах, которые мы отправляем, вы можете изменить предпочтения и легко отписаться в любой момент."
|
||||
cost_title: "Стоимость"
|
||||
cost_description: "В настоящее время, CodeCombat 100% бесплатен! Одной из наших главных целей является сохранить его таким, чтобы как можно больше людей могли играть, независимо от места в жизни. Если небо потемнеет, мы, возможно, введём подписки, возможно, только на некоторый контент, но нам не хотелось бы. Если повезёт, мы сможем поддерживать компанию, используя"
|
||||
#cost_description: "В настоящее время, CodeCombat 100% бесплатен! Одной из наших главных целей является сохранить его таким, чтобы как можно больше людей могли играть, независимо от места в жизни. Если небо потемнеет, мы, возможно, введём подписки, возможно, только на некоторый контент, но нам не хотелось бы. Если повезёт, мы сможем поддерживать компанию, используя" # Please update based on new wording from en.coffee
|
||||
copyrights_title: "Авторские права и лицензии"
|
||||
contributor_title: "Лицензионное соглашение соавторов"
|
||||
contributor_description_prefix: "Все вклады, как на сайте, так и на нашем репозитории GitHub, подпадают под наше"
|
||||
|
|
|
@ -102,19 +102,6 @@ module.exports = class User extends CocoModel
|
|||
myHeroClasses.push heroClass for heroClass, heroSlugs of ThangType.heroClasses when _.intersection(myHeroSlugs, heroSlugs).length
|
||||
myHeroClasses
|
||||
|
||||
getBranchingGroup: ->
|
||||
return @branchingGroup if @branchingGroup
|
||||
return 'all-practice' # A/B test paused for Hour of Code
|
||||
group = me.get('testGroupNumber') % 4
|
||||
@branchingGroup = switch group
|
||||
when 0 then 'no-practice'
|
||||
when 1 then 'all-practice'
|
||||
when 2 then 'choice-explicit'
|
||||
when 3 then 'choice-implicit'
|
||||
@branchingGroup = 'choice-explicit' if me.isAdmin()
|
||||
application.tracker.identify branchingGroup: @branchingGroup unless me.isAdmin()
|
||||
@branchingGroup
|
||||
|
||||
getGemPromptGroup: ->
|
||||
return @gemPromptGroup if @gemPromptGroup
|
||||
group = me.get('testGroupNumber') % 8
|
||||
|
@ -125,6 +112,13 @@ module.exports = class User extends CocoModel
|
|||
application.tracker.identify gemPromptGroup: @gemPromptGroup unless me.isAdmin()
|
||||
@gemPromptGroup
|
||||
|
||||
isPremium: ->
|
||||
return false unless stripe = @get('stripe')
|
||||
return true if stripe.subscriptionID
|
||||
return true if stripe.free is true
|
||||
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
|
||||
return false
|
||||
|
||||
tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15
|
||||
]
|
||||
|
|
|
@ -221,16 +221,6 @@
|
|||
.last-submitted
|
||||
float: none
|
||||
|
||||
.next-levels-prompt
|
||||
display: none
|
||||
margin: 30px -21px
|
||||
|
||||
.btn
|
||||
width: 30%
|
||||
width: -webkit-calc(33.333333% - 10px)
|
||||
width: calc(33.333333% - 10px)
|
||||
margin: 5px
|
||||
|
||||
.hour-of-code-done
|
||||
clear: both
|
||||
padding-top: 10px
|
||||
|
|
|
@ -107,9 +107,6 @@
|
|||
.executed
|
||||
background-color: rgba(110, 110, 110, 0.12)
|
||||
|
||||
.locked-code
|
||||
border: 1px dashed rgba(53, 45, 34, 0.5)
|
||||
|
||||
+keyframes(pulseRedBackground)
|
||||
from
|
||||
background-color: rgba(255, 45, 27, 0.4)
|
||||
|
@ -175,6 +172,9 @@
|
|||
// Override faint gray
|
||||
border-color: #BFF
|
||||
|
||||
.locked-code
|
||||
border: 1px dashed rgba(53, 45, 34, 0.5)
|
||||
|
||||
// Decided it wasn't useful to show what can be hovered, since almost anything can, so we have to make it too faint to be useful if we don't want it to be really distracting.
|
||||
//.ace_identifier
|
||||
// border-bottom: 1px dotted rgba(0, 51, 255, 0.25)
|
||||
|
|
|
@ -231,7 +231,7 @@ $gameControlMargin: 30px
|
|||
font-size: 2vw
|
||||
text-shadow: 0 0 0.3vw white, 0 0 0.3vw white
|
||||
|
||||
&:hover
|
||||
&:hover
|
||||
text-decoration: none
|
||||
|
||||
&#forest-link
|
||||
|
@ -240,9 +240,10 @@ $gameControlMargin: 30px
|
|||
transform: rotate(-35deg)
|
||||
|
||||
&#dungeon-link
|
||||
left: 13.01%
|
||||
top: 58%
|
||||
left: 9%
|
||||
top: 54.5%
|
||||
transform: rotate(180deg)
|
||||
color: fuchsia
|
||||
|
||||
.game-controls
|
||||
position: absolute
|
||||
|
|
|
@ -42,11 +42,6 @@ block modal-body-content
|
|||
img(src=item.getPortraitURL())
|
||||
.reward-text= animate ? 'New Item' : item.get('name')
|
||||
|
||||
.next-levels-prompt
|
||||
for button in continueButtons
|
||||
- var enabled = Boolean(button.link != '/play' || me.getBranchingGroup() == 'choice-implicit' || button.key == 'continue');
|
||||
button.btn.btn-success.btn-lg.world-map-button.next-level-branch-button(data-href=button.link, disabled=!enabled, data-dismiss="modal", data-i18n="play_level.victory_play_" + button[me.getBranchingGroup()], data-branch-key=button.key)
|
||||
|
||||
block modal-footer-content
|
||||
if me.get('anonymous')
|
||||
p.sign-up-poke.hide
|
||||
|
|
|
@ -16,18 +16,17 @@
|
|||
|
||||
#selling-points
|
||||
#point-levels.point
|
||||
.blurb(data-i18n="subscribe.levels") 25 more levels, with 5 new levels every week!
|
||||
.blurb(data-i18n="subscribe.levels")
|
||||
#point-heroes.point
|
||||
.blurb(data-i18n="subscribe.heroes") Unlock more heroes, including wizards and rangers!
|
||||
.blurb(data-i18n="subscribe.heroes")
|
||||
#point-gems.point
|
||||
.blurb(data-i18n="subscribe.gems") Subscribers get 3500 bonus gems per month!
|
||||
.blurb(data-i18n="subscribe.gems")
|
||||
#point-items.point
|
||||
.blurb(data-i18n="subscribe.items") Unlock the coding power of 275 new items!
|
||||
.blurb(data-i18n="subscribe.items")
|
||||
|
||||
#parents-info(data-i18n="subscribe.parents")
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
|
||||
span $9.99/mo - Subscribe
|
||||
|
||||
if state === 'declined'
|
||||
#declined-alert.alert.alert-danger.alert-dismissible
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
- var seenNext = nextLevel;
|
||||
each level in campaign.levels
|
||||
if !level.hidden
|
||||
- var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled && (!level.practice || me.getBranchingGroup() == 'all-practice'));
|
||||
- var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled);
|
||||
- seenNext = seenNext || next;
|
||||
div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
|
||||
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
||||
|
@ -73,15 +73,15 @@ button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume")
|
|||
.glyphicon.glyphicon-volume-down
|
||||
.glyphicon.glyphicon-volume-up
|
||||
|
||||
h1#campaign-status
|
||||
if mapType == 'dungeon'
|
||||
span.spr(data-i18n="play.campaign_dungeon")
|
||||
else if mapType == 'forest'
|
||||
span.spr(data-i18n="play.campaign_forest")
|
||||
| -
|
||||
if requiresSubscription
|
||||
span.spl(data-i18n="play.subscription_required")
|
||||
else if mapType == 'dungeon'
|
||||
span.spl(data-i18n="play.free")
|
||||
else
|
||||
span.spl(data-i18n="play.subscribed")
|
||||
//h1#campaign-status
|
||||
// if mapType == 'dungeon'
|
||||
// span.spr(data-i18n="play.campaign_dungeon")
|
||||
// else if mapType == 'forest'
|
||||
// span.spr(data-i18n="play.campaign_forest")
|
||||
// | -
|
||||
// if requiresSubscription
|
||||
// span.spl(data-i18n="play.subscription_required")
|
||||
// else if mapType == 'dungeon'
|
||||
// span.spl(data-i18n="play.free")
|
||||
// else
|
||||
// span.spl(data-i18n="play.subscribed")
|
|
@ -24,7 +24,7 @@ module.exports = class PaymentsView extends RootView
|
|||
c = super()
|
||||
c.payments = @payments
|
||||
c.subscribed = me.get('stripe')?.planID
|
||||
c.active = me.get('stripe')?.subscriptionID
|
||||
c.active = me.isPremium()
|
||||
c
|
||||
|
||||
onClickStartSubscription: (e) ->
|
||||
|
|
|
@ -81,7 +81,7 @@ module.exports = class WorldMapView extends RootView
|
|||
$('body').append($('<img src="http://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
|
||||
trackedHourOfCode = true
|
||||
|
||||
@requiresSubscription = @terrain isnt 'dungeon' and not me.get('stripe')?.subscriptionID
|
||||
@requiresSubscription = @terrain isnt 'dungeon' and not me.isPremium()
|
||||
|
||||
destroy: ->
|
||||
@setupManager?.destroy()
|
||||
|
@ -118,14 +118,6 @@ module.exports = class WorldMapView extends RootView
|
|||
@fullyRendered = true
|
||||
@render()
|
||||
@preloadTopHeroes() unless me.get('heroConfig')?.thangType
|
||||
if @requiresSubscription
|
||||
modal = if me.get('anonymous') then AuthModal else SubscribeModal
|
||||
_.delay =>
|
||||
@openModalView? new modal() unless window.currentModal
|
||||
if modal is SubscribeModal
|
||||
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'world map loadded'
|
||||
window.tracker?.trackPageView "subscription/show-modal", ['Google Analytics']
|
||||
, 2000
|
||||
|
||||
onSubscribed: ->
|
||||
@requiresSubscription = false
|
||||
|
@ -144,9 +136,8 @@ module.exports = class WorldMapView extends RootView
|
|||
level.locked = false if me.get('slug') is 'nick'
|
||||
level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
|
||||
level.color = 'rgb(255, 80, 60)'
|
||||
if level.practice
|
||||
level.color = 'rgb(80, 130, 200)' unless me.getBranchingGroup() is 'all-practice'
|
||||
level.hidden = true if me.getBranchingGroup() is 'no-practice'
|
||||
if level.requiresSubscription
|
||||
level.color = 'rgb(80, 130, 200)'
|
||||
context.levelStatusMap = @levelStatusMap
|
||||
context.levelPlayCountMap = @levelPlayCountMap
|
||||
context.isIPadApp = application.isIPadApp
|
||||
|
@ -161,10 +152,16 @@ module.exports = class WorldMapView extends RootView
|
|||
@onWindowResize()
|
||||
unless application.isIPadApp
|
||||
_.defer => @$el?.find('.game-controls .btn').tooltip() # Have to defer or i18n doesn't take effect.
|
||||
@$el.find('.level').tooltip()
|
||||
@$el.find('.level').tooltip().each ->
|
||||
return unless me.isAdmin()
|
||||
$(@).draggable().on 'dragstop', ->
|
||||
bg = $('.map-background')
|
||||
x = ($(@).offset().left - bg.offset().left + $(@).outerWidth() / 2) / bg.width()
|
||||
y = 1 - ($(@).offset().top - bg.offset().top + $(@).outerHeight() / 2) / bg.height()
|
||||
console.log "#{$(@).data('level-id')}\n x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n"
|
||||
@$el.addClass _.string.slugify @terrain
|
||||
@updateVolume()
|
||||
unless window.currentModal or not @fullyRendered or @requiresSubscription
|
||||
unless window.currentModal or not @fullyRendered
|
||||
@highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top']
|
||||
if levelID = @$el.find('.level.next').data('level-id')
|
||||
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
|
||||
|
@ -210,7 +207,7 @@ module.exports = class WorldMapView extends RootView
|
|||
@adjustLevelInfoPosition e
|
||||
@endHighlight()
|
||||
else
|
||||
if @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer
|
||||
if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer
|
||||
modal = if me.get('anonymous') then AuthModal else SubscribeModal
|
||||
@openModalView new modal()
|
||||
if modal is SubscribeModal
|
||||
|
@ -376,8 +373,8 @@ dungeon = [
|
|||
id: 'shadow-guard'
|
||||
original: '54174347844506ae0195a0b8'
|
||||
description: 'Evade the Kithgard minion.'
|
||||
x: 44
|
||||
y: 11
|
||||
x: 40.54
|
||||
y: 11.03
|
||||
nextLevels:
|
||||
more_practice: 'kounter-kithwise'
|
||||
continue: 'forgetful-gemsmith'
|
||||
|
@ -388,33 +385,34 @@ dungeon = [
|
|||
id: 'kounter-kithwise'
|
||||
original: '54527a6257e83800009730c7'
|
||||
description: 'Practice your evasion skills with more guards.'
|
||||
x: 55
|
||||
y: 11
|
||||
x: 35.37
|
||||
y: 20.61
|
||||
nextLevels:
|
||||
continue: 'crawlways-of-kithgard'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Crawlways of Kithgard'
|
||||
type: 'hero'
|
||||
id: 'crawlways-of-kithgard'
|
||||
original: '545287ef57e83800009730d5'
|
||||
description: 'Dart in and grab the gem–at the right moment.'
|
||||
x: 36.48
|
||||
y: 29.03
|
||||
nextLevels:
|
||||
#more_practice: 'crawlways-of-kithgard'
|
||||
continue: 'forgetful-gemsmith'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
#{
|
||||
# name: 'Crawlways of Kithgard'
|
||||
# type: 'hero'
|
||||
# # id: 'crawlways-of-kithgard'
|
||||
# original: '545287ef57e83800009730d5'
|
||||
# description: 'Dart in and grab the gem–at the right moment.'
|
||||
# x: 57
|
||||
# y: 12
|
||||
# nextLevels:
|
||||
# continue: 'true-names'
|
||||
# practice: true
|
||||
#}
|
||||
{
|
||||
name: 'Forgetful Gemsmith'
|
||||
type: 'hero'
|
||||
id: 'forgetful-gemsmith'
|
||||
original: '544a98f62d002f0000fe331a'
|
||||
description: 'Grab even more gems as you practice moving.'
|
||||
x: 66
|
||||
y: 11
|
||||
x: 54.98
|
||||
y: 10.53
|
||||
nextLevels:
|
||||
continue: 'true-names'
|
||||
}
|
||||
|
@ -424,8 +422,8 @@ dungeon = [
|
|||
id: 'true-names'
|
||||
original: '541875da4c16460000ab990f'
|
||||
description: 'Learn an enemy\'s true name to defeat it.'
|
||||
x: 76
|
||||
y: 13
|
||||
x: 68.44
|
||||
y: 10.70
|
||||
nextLevels:
|
||||
more_practice: 'favorable-odds'
|
||||
continue: 'the-raised-sword'
|
||||
|
@ -436,11 +434,12 @@ dungeon = [
|
|||
id: 'favorable-odds'
|
||||
original: '5452972f57e83800009730de'
|
||||
description: 'Test out your battle skills by defeating more munchkins.'
|
||||
x: 80.85
|
||||
y: 16
|
||||
x: 88.25
|
||||
y: 14.92
|
||||
nextLevels:
|
||||
continue: 'the-raised-sword'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'The Raised Sword'
|
||||
|
@ -448,23 +447,11 @@ dungeon = [
|
|||
id: 'the-raised-sword'
|
||||
original: '5418aec24c16460000ab9aa6'
|
||||
description: 'Learn to equip yourself for combat.'
|
||||
x: 85
|
||||
y: 20
|
||||
x: 81.51
|
||||
y: 17.92
|
||||
nextLevels:
|
||||
continue: 'haunted-kithmaze'
|
||||
}
|
||||
#{
|
||||
# name: 'The First Kithmaze'
|
||||
# type: 'hero'
|
||||
# id: 'the-first-kithmaze'
|
||||
# original: '5418b9d64c16460000ab9ab4'
|
||||
# description: 'The builders of Kithgard constructed many mazes to confuse travelers.'
|
||||
# x: 78
|
||||
# y: 29
|
||||
# nextLevels:
|
||||
# more_practice: 'descending-further'
|
||||
# continue: 'the-second-kithmaze'
|
||||
#}
|
||||
{
|
||||
name: 'Haunted Kithmaze'
|
||||
type: 'hero'
|
||||
|
@ -477,17 +464,32 @@ dungeon = [
|
|||
more_practice: 'descending-further'
|
||||
continue: 'the-second-kithmaze'
|
||||
}
|
||||
{
|
||||
name: 'Riddling Kithmaze'
|
||||
type: 'hero'
|
||||
id: 'riddling-kithmaze'
|
||||
original: '5418b9d64c16460000ab9ab4'
|
||||
description: 'If at first you go astray, change your loop to find the way.'
|
||||
x: 69.97
|
||||
y: 28.03
|
||||
nextLevels:
|
||||
more_practice: 'descending-further'
|
||||
continue: 'the-second-kithmaze'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Descending Further'
|
||||
type: 'hero'
|
||||
id: 'descending-further'
|
||||
original: '5452a84d57e83800009730e4'
|
||||
description: 'Another day, another maze.'
|
||||
x: 70
|
||||
y: 28
|
||||
x: 61.68
|
||||
y: 22.80
|
||||
nextLevels:
|
||||
continue: 'the-second-kithmaze'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'The Second Kithmaze'
|
||||
|
@ -495,8 +497,8 @@ dungeon = [
|
|||
id: 'the-second-kithmaze'
|
||||
original: '5418cf256bae62f707c7e1c3'
|
||||
description: 'Many have tried, few have found their way through this maze.'
|
||||
x: 58
|
||||
y: 23
|
||||
x: 54.49
|
||||
y: 26.49
|
||||
nextLevels:
|
||||
continue: 'dread-door'
|
||||
}
|
||||
|
@ -506,8 +508,8 @@ dungeon = [
|
|||
id: 'dread-door'
|
||||
original: '5418d40f4c16460000ab9ac2'
|
||||
description: 'Behind a dread door lies a chest full of riches.'
|
||||
x: 59
|
||||
y: 32
|
||||
x: 60.52
|
||||
y: 33.70
|
||||
nextLevels:
|
||||
continue: 'known-enemy'
|
||||
}
|
||||
|
@ -562,11 +564,12 @@ dungeon = [
|
|||
id: 'tactical-strike'
|
||||
original: '5452cfa706a59e000067e4f5'
|
||||
description: 'They\'re, uh, coming right for us! Sneak up behind them.'
|
||||
x: 88.65
|
||||
y: 63.06
|
||||
x: 83.23
|
||||
y: 52.73
|
||||
nextLevels:
|
||||
continue: 'the-final-kithmaze'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'The Final Kithmaze'
|
||||
|
@ -574,8 +577,8 @@ dungeon = [
|
|||
id: 'the-final-kithmaze'
|
||||
original: '541b434e1ccc8eaae19f3c33'
|
||||
description: 'To escape you must find your way through an Elder Kithman\'s maze.'
|
||||
x: 83
|
||||
y: 68
|
||||
x: 86.95
|
||||
y: 64.70
|
||||
nextLevels:
|
||||
more_practice: 'the-gauntlet'
|
||||
continue: 'kithgard-gates'
|
||||
|
@ -586,11 +589,12 @@ dungeon = [
|
|||
id: 'the-gauntlet'
|
||||
original: '5452d8b906a59e000067e4fa'
|
||||
description: 'Rush for the stairs, battling foes at every turn.'
|
||||
x: 84.89
|
||||
y: 73.88
|
||||
x: 76.50
|
||||
y: 72.69
|
||||
nextLevels:
|
||||
continue: 'kithgard-gates'
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Kithgard Gates'
|
||||
|
@ -658,6 +662,8 @@ forest = [
|
|||
continue: 'thornbush-farm'
|
||||
x: 33
|
||||
y: 37
|
||||
practice: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Thornbush Farm'
|
||||
|
@ -750,6 +756,7 @@ forest = [
|
|||
continue: 'swift-dagger'
|
||||
x: 38
|
||||
y: 72
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Swift Dagger'
|
||||
|
@ -761,6 +768,7 @@ forest = [
|
|||
continue: 'shrapnel'
|
||||
x: 33
|
||||
y: 72
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Shrapnel'
|
||||
|
@ -772,6 +780,7 @@ forest = [
|
|||
continue: 'coinucopia'
|
||||
x: 28
|
||||
y: 73
|
||||
requiresSubscription: true
|
||||
}
|
||||
|
||||
# Wizard branch
|
||||
|
@ -786,6 +795,7 @@ forest = [
|
|||
x: 47
|
||||
y: 71
|
||||
adventurer: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Touch of Death'
|
||||
|
@ -798,6 +808,7 @@ forest = [
|
|||
x: 52
|
||||
y: 70
|
||||
adventurer: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Bonemender'
|
||||
|
@ -810,6 +821,7 @@ forest = [
|
|||
x: 58
|
||||
y: 67
|
||||
adventurer: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -855,6 +867,7 @@ forest = [
|
|||
continue: 'rich-forager'
|
||||
x: 74.5
|
||||
y: 92
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Rich Forager'
|
||||
|
@ -867,6 +880,7 @@ forest = [
|
|||
x: 80
|
||||
y: 88
|
||||
adventurer: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Siege of Stonehold'
|
||||
|
@ -880,6 +894,7 @@ forest = [
|
|||
x: 85.5
|
||||
y: 83.5
|
||||
adventurer: true
|
||||
requiresSubscription: true
|
||||
}
|
||||
{
|
||||
name: 'Multiplayer Treasure Grove'
|
||||
|
|
|
@ -21,7 +21,6 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
|
||||
events:
|
||||
'click #continue-button': 'onClickContinue'
|
||||
'click .next-level-branch-button': 'onClickNextLevelBranch'
|
||||
'click .return-to-ladder-button': 'onClickReturnToLadder'
|
||||
|
||||
constructor: (options) ->
|
||||
|
@ -110,13 +109,6 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()
|
||||
c.level = @level
|
||||
@continueLevelLink = @getNextLevelLink 'continue'
|
||||
@morePracticeLevelLink = @getNextLevelLink 'more_practice'
|
||||
@skipAheadLevelLink = @getNextLevelLink 'skip_ahead'
|
||||
c.continueButtons = [
|
||||
{key: 'skip_ahead', link: @skipAheadLevelLink, 'choice-explicit': 'skip', 'choice-implicit': 'too_easy'}
|
||||
{key: 'continue', link: @continueLevelLink, 'choice-explicit': 'next_level', 'choice-implicit': 'just_right'}
|
||||
{key: 'more_practice', link: @morePracticeLevelLink, 'choice-explicit': 'more_practice', 'choice-implicit': 'too_hard'}
|
||||
]
|
||||
|
||||
elapsed = (new Date() - new Date(me.get('dateCreated')))
|
||||
isHourOfCode = me.get('hourOfCode') or elapsed < 120 * 60 * 1000
|
||||
|
@ -290,34 +282,15 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
return link unless nextLevel = @getNextLevel type
|
||||
"#{link}?next=#{nextLevel}"
|
||||
|
||||
# Branching group testing
|
||||
|
||||
getNextLevel: (type) ->
|
||||
levelInfo = @getLevelInfoForSlug @level.get 'slug'
|
||||
levelInfo?.nextLevels?[type] # 'more_practice', 'skip_ahead', 'continue'
|
||||
levelInfo?.nextLevels?[type] # 'continue'; TODO: refactor to not have the object and just use single nextLevel property
|
||||
|
||||
onClickContinue: (e) ->
|
||||
@playSound 'menu-button-click'
|
||||
nextLevelLink = @continueLevelLink
|
||||
if me.getBranchingGroup() is 'all-practice' and @morePracticeLevelLink
|
||||
nextLevelLink = @morePracticeLevelLink
|
||||
skipPrompt = me.getBranchingGroup() in ['no-practice', 'all-practice']
|
||||
skipPrompt ||= not (@skipAheadLevelLink or @morePractiveLevelLink) and me.getBranchingGroup() is 'choice-explicit'
|
||||
if skipPrompt
|
||||
# Preserve the supermodel as we navigate back to the world map.
|
||||
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
|
||||
else
|
||||
# Hide everything except the buttons prompting them for which kind of next level to do
|
||||
@$el.find('.modal-footer, .modal-body > *').hide()
|
||||
@$el.find('.next-levels-prompt').show()
|
||||
|
||||
onClickNextLevelBranch: (e) ->
|
||||
@playSound 'menu-button-click'
|
||||
e.preventDefault()
|
||||
route = $(e.target).data('href') or "/play/#{@getNextLevelCampaign()}"
|
||||
application.tracker?.trackEvent 'Branch Selected', level: @level.get('slug'), label: @level.get('slug'), branch: $(e.target).data('branch-key'), branchingGroup: me.getBranchingGroup(), route: route
|
||||
# Preserve the supermodel as we navigate back to world map.
|
||||
Backbone.Mediator.publish 'router:navigate', route: route, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
|
||||
# Preserve the supermodel as we navigate back to the world map.
|
||||
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
|
||||
|
||||
onClickReturnToLadder: (e) ->
|
||||
@playSound 'menu-button-click'
|
||||
|
|
|
@ -257,7 +257,6 @@ module.exports = class SpellView extends CocoView
|
|||
# TODO: Lock default indent for an empty line?
|
||||
return unless LevelOptions[@options.level.get('slug')]?.lockDefaultCode or CampaignOptions?.getOption?(@options?.level?.get?('slug'), 'lockDefaultCode')
|
||||
return unless @spell.source is @spell.originalSource or force
|
||||
return if @spell.language in ['javascript', 'lua', 'clojure', 'io'] # Only works for languages without closing delimeters on blocks currently
|
||||
|
||||
console.info 'Locking down default code.'
|
||||
|
||||
|
@ -294,16 +293,50 @@ module.exports = class SpellView extends CocoView
|
|||
wrapper => orig.apply obj, args
|
||||
obj[method]
|
||||
|
||||
if @lockedCodeMarkerID?
|
||||
@aceSession.removeMarker @lockedCodeMarkerID
|
||||
@lockedCodeMarkerID = null
|
||||
|
||||
finishRange = (row, startRow, startColumn) =>
|
||||
range = new Range startRow, startColumn, row, @aceSession.getLine(row).length - 1
|
||||
range.start = @aceDoc.createAnchor range.start
|
||||
range.end = @aceDoc.createAnchor range.end
|
||||
range.end.$insertRight = true
|
||||
@readOnlyRanges.push range
|
||||
|
||||
# Remove previous locked code highlighting
|
||||
if @lockedCodeMarkerIDs?
|
||||
@aceSession.removeMarker marker for marker in @lockedCodeMarkerIDs
|
||||
@lockedCodeMarkerIDs = []
|
||||
|
||||
# Create locked default code text ranges
|
||||
@readOnlyRanges = []
|
||||
lines = @aceDoc.getAllLines()
|
||||
lastRow = row for line, row in lines when not /^\s*$/.test(line)
|
||||
if lastRow?
|
||||
@readOnlyRanges.push new Range 0, 0, lastRow, lines[lastRow].length - 1
|
||||
@lockedCodeMarkerID = @aceSession.addMarker @readOnlyRanges[0], 'locked-code', 'fullLine'
|
||||
if @spell.language in ['python', 'coffeescript']
|
||||
# Lock contiguous section of default code
|
||||
# Only works for languages without closing delimeters on blocks currently
|
||||
lines = @aceDoc.getAllLines()
|
||||
lastRow = row for line, row in lines when not /^\s*$/.test(line)
|
||||
if lastRow?
|
||||
@readOnlyRanges.push new Range 0, 0, lastRow, lines[lastRow].length - 1
|
||||
|
||||
# TODO: Highlighting does not work for multiple ranges
|
||||
# TODO: Everything looks correct except the actual result.
|
||||
# TODO: https://github.com/codecombat/codecombat/issues/1852
|
||||
# else
|
||||
# # Create a read-only range for each chunk of text not separated by an empty line
|
||||
# startRow = startColumn = null
|
||||
# for row in [0...@aceSession.getLength()]
|
||||
# unless /^\s*$/.test @aceSession.getLine(row)
|
||||
# unless startRow? and startColumn?
|
||||
# startRow = row
|
||||
# startColumn = 0
|
||||
# else
|
||||
# if startRow? and startColumn?
|
||||
# finishRange row - 1, startRow, startColumn
|
||||
# startRow = startColumn = null
|
||||
# if startRow? and startColumn?
|
||||
# finishRange @aceSession.getLength() - 1, startRow, startColumn
|
||||
|
||||
# Highlight locked ranges
|
||||
for range in @readOnlyRanges
|
||||
@lockedCodeMarkerIDs.push @aceSession.addMarker range, 'locked-code', 'fullLine'
|
||||
|
||||
# Override write operations that intersect with default code
|
||||
interceptCommand @ace, 'onPaste', preventReadonly
|
||||
|
|
|
@ -72,7 +72,7 @@ LevelHandler = class LevelHandler extends Handler
|
|||
Session.findOne(sessionQuery).exec (err, doc) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendSuccess(res, doc) if doc?
|
||||
return @sendPaymentRequiredError(res, err) if (not req.user.get('stripe')?.subscriptionID) and level.get('requiresSubscription')
|
||||
return @sendPaymentRequiredError(res, err) if (not req.user.isPremium()) and level.get('requiresSubscription')
|
||||
@createAndSaveNewSession sessionQuery, req, res
|
||||
|
||||
createAndSaveNewSession: (sessionQuery, req, res) =>
|
||||
|
|
|
@ -8,7 +8,6 @@ sendwithus = require '../sendwithus'
|
|||
hipchat = require '../hipchat'
|
||||
config = require '../../server_config'
|
||||
request = require 'request'
|
||||
stripe = require('stripe')(config.stripe.secretKey)
|
||||
async = require 'async'
|
||||
|
||||
products = {
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
# the stripe property in the user with what's being stored in Stripe.
|
||||
|
||||
Handler = require '../commons/Handler'
|
||||
config = require '../../server_config'
|
||||
stripe = require('stripe')(config.stripe.secretKey)
|
||||
discountHandler = require './discount_handler'
|
||||
|
||||
subscriptions = {
|
||||
|
@ -13,17 +11,17 @@ subscriptions = {
|
|||
}
|
||||
|
||||
class SubscriptionHandler extends Handler
|
||||
logSubscriptionError: (req, msg) ->
|
||||
console.warn "Subscription Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'"
|
||||
logSubscriptionError: (user, msg) ->
|
||||
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
|
||||
|
||||
subscribeUser: (req, user, done) ->
|
||||
if (not req.user) or req.user.isAnonymous()
|
||||
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
|
||||
return done({res: 'You must be signed in to subscribe.', code: 403})
|
||||
|
||||
token = req.body.stripe.token
|
||||
customerID = user.get('stripe')?.customerID
|
||||
if not (token or customerID)
|
||||
@logSubscriptionError(req, 'Missing stripe token or customer ID.')
|
||||
@logSubscriptionError(user, 'Missing stripe token or customer ID.')
|
||||
return done({res: 'Missing stripe token or customer ID.', code: 422})
|
||||
|
||||
if token
|
||||
|
@ -31,15 +29,15 @@ class SubscriptionHandler extends Handler
|
|||
stripe.customers.update customerID, { card: token }, (err, customer) =>
|
||||
if err or not customer
|
||||
# should not happen outside of test and production polluting each other
|
||||
@logSubscriptionError(req, 'Cannot find customer: ', +customer.id + '\n\n' + err)
|
||||
@logSubscriptionError(user, 'Cannot find customer: ', +customer.id + '\n\n' + err)
|
||||
return done({res: 'Cannot find customer.', code: 404})
|
||||
@checkForExistingSubscription(req, user, customer, done)
|
||||
|
||||
else
|
||||
newCustomer = {
|
||||
card: token
|
||||
email: req.user.get('email')
|
||||
metadata: { id: req.user._id + '', slug: req.user.get('slug') }
|
||||
email: user.get('email')
|
||||
metadata: { id: user._id + '', slug: user.get('slug') }
|
||||
}
|
||||
|
||||
stripe.customers.create newCustomer, (err, customer) =>
|
||||
|
@ -47,22 +45,22 @@ class SubscriptionHandler extends Handler
|
|||
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
|
||||
return done({res: 'Card error', code: 402})
|
||||
else
|
||||
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
|
||||
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
|
||||
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
|
||||
stripeInfo.customerID = customer.id
|
||||
req.user.set('stripe', stripeInfo)
|
||||
req.user.save (err) =>
|
||||
user.set('stripe', stripeInfo)
|
||||
user.save (err) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe customer id save db error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer id save db error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
@checkForExistingSubscription(req, user, customer, done)
|
||||
|
||||
else
|
||||
stripe.customers.retrieve(customerID, (err, customer) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
@checkForExistingSubscription(req, user, customer, done)
|
||||
)
|
||||
|
@ -79,14 +77,14 @@ class SubscriptionHandler extends Handler
|
|||
# subscription a trial period that ends when the cancelled subscription would have ended.
|
||||
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe cancel subscription error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
|
||||
options = { plan: 'basic', trial_end: subscription.current_period_end }
|
||||
options.coupon = couponID if couponID
|
||||
stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) =>
|
||||
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe customer plan setting error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer plan setting error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
|
||||
@updateUser(req, user, customer, false, done)
|
||||
|
@ -98,9 +96,9 @@ class SubscriptionHandler extends Handler
|
|||
else
|
||||
options = { plan: 'basic' }
|
||||
options.coupon = couponID if couponID
|
||||
stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) =>
|
||||
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe customer plan setting error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe customer plan setting error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
|
||||
@updateUser(req, user, customer, true, done)
|
||||
|
@ -123,25 +121,25 @@ class SubscriptionHandler extends Handler
|
|||
|
||||
user.save (err) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe user plan saving error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe user plan saving error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
req.user?.saveActiveUser 'subscribe'
|
||||
user?.saveActiveUser 'subscribe'
|
||||
return done()
|
||||
|
||||
unsubscribeUser: (req, user, done) ->
|
||||
stripeInfo = _.cloneDeep(user.get('stripe'))
|
||||
stripe.customers.cancelSubscription stripeInfo.customerID, stripeInfo.subscriptionID, { at_period_end: true }, (err) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'Stripe cancel subscription error. '+err)
|
||||
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
delete stripeInfo.planID
|
||||
user.set('stripe', stripeInfo)
|
||||
req.body.stripe = stripeInfo
|
||||
user.save (err) =>
|
||||
if err
|
||||
@logSubscriptionError(req, 'User save unsubscribe error. '+err)
|
||||
@logSubscriptionError(user, 'User save unsubscribe error. '+err)
|
||||
return done({res: 'Database error.', code: 500})
|
||||
req.user?.saveActiveUser 'unsubscribe'
|
||||
user?.saveActiveUser 'unsubscribe'
|
||||
return done()
|
||||
|
||||
module.exports = new SubscriptionHandler()
|
||||
|
|
|
@ -179,6 +179,13 @@ UserSchema.methods.register = (done) ->
|
|||
delighted.addDelightedUser @
|
||||
@saveActiveUser 'register'
|
||||
|
||||
UserSchema.methods.isPremium = ->
|
||||
return false unless stripe = @get('stripe')
|
||||
return true if stripe.subscriptionID
|
||||
return true if stripe.free is true
|
||||
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
|
||||
return false
|
||||
|
||||
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
|
||||
id = mongoose.Types.ObjectId id if _.isString id
|
||||
@findById id, (err, user) ->
|
||||
|
|
|
@ -230,9 +230,19 @@ UserHandler = class UserHandler extends Handler
|
|||
return @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2]
|
||||
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
||||
return @searchForUser(req, res) if args[1] is 'admin_search'
|
||||
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
||||
return @sendNotFoundError(res)
|
||||
super(arguments...)
|
||||
|
||||
getStripeInfo: (req, res, handle) ->
|
||||
@getDocumentForIdOrSlug handle, (err, user) =>
|
||||
return @sendNotFoundError(res) if not user
|
||||
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or req.user.get('_id').equals(user.get('_id')))
|
||||
return @sendNotFoundError(res) #if not customerID = user.get('stripe')?.customerID
|
||||
stripe.customers.retrieve customerID, (err, customer) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendSuccess(res, JSON.stringify(customer, null, '\t'))
|
||||
|
||||
agreeToCLA: (req, res) ->
|
||||
return @sendForbiddenError(res) unless req.user
|
||||
doc =
|
||||
|
|
|
@ -16,6 +16,8 @@ UserHandler = require './server/users/user_handler'
|
|||
hipchat = require './server/hipchat'
|
||||
global.tv4 = require 'tv4' # required for TreemaUtils to work
|
||||
global.jsondiffpatch = require 'jsondiffpatch'
|
||||
global.stripe = require('stripe')(config.stripe.secretKey)
|
||||
|
||||
|
||||
productionLogging = (tokens, req, res) ->
|
||||
status = res.statusCode
|
||||
|
|