Implementing alternative pricing with Alipay in China to support dedicated China server.

This commit is contained in:
Nick Winter 2015-03-23 15:26:44 -07:00
parent 26b7c65855
commit b4ea78e5cb
22 changed files with 100 additions and 53 deletions

View file

@ -4,7 +4,7 @@ module.exports = handler = StripeCheckout.configure({
key: publishableKey key: publishableKey
name: 'CodeCombat' name: 'CodeCombat'
email: me.get('email') email: me.get('email')
image: '/images/pages/base/logo_square_250.png' image: "https://codecombat.com/images/pages/base/logo_square_250.png"
token: (token) -> token: (token) ->
Backbone.Mediator.publish 'stripe:received-token', { token: token } Backbone.Mediator.publish 'stripe:received-token', { token: token }
locale: 'auto' locale: 'auto'

View file

@ -581,13 +581,14 @@
intro_1: "CodeCombat is an online game that teaches programming. Students write code in real programming languages." intro_1: "CodeCombat is an online game that teaches programming. Students write code in real programming languages."
intro_2: "No experience required!" intro_2: "No experience required!"
free_title: "How much does it cost?" free_title: "How much does it cost?"
cost_china: "CodeCombat in China is free for the first five levels, after which it costs $9.99 per month for access to our other 120+ levels on our exclusive China servers."
free_1: "CodeCombat Basic is FREE! There are 70+ free levels which cover every concept." free_1: "CodeCombat Basic is FREE! There are 70+ free levels which cover every concept."
free_2: "A monthly subscription provides access to video tutorials and extra practice levels." free_2: "A monthly subscription provides access to video tutorials and extra practice levels."
teacher_subs_title: "Teachers get free subscriptions!" teacher_subs_title: "Teachers get free subscriptions!"
teacher_subs_1: "Please contact" teacher_subs_1: "Please contact"
teacher_subs_2: "to setup a free monthly subscription." teacher_subs_2: "to set up a free monthly subscription."
sub_includes_title: "What is included in the subscription?" sub_includes_title: "What is included in the subscription?"
sub_includes_1: "In additional to the 70+ free levels, students with a monthly subscription get access to these additional features:" sub_includes_1: "In additional to the 70+ basic levels, students with a monthly subscription get access to these additional features:" # {change}
sub_includes_2: "40+ practice levels" sub_includes_2: "40+ practice levels"
sub_includes_3: "Video tutorials" sub_includes_3: "Video tutorials"
sub_includes_4: "Premium email support" sub_includes_4: "Premium email support"
@ -597,7 +598,8 @@
who_for_1: "We recommend CodeCombat for students aged 9 and up. No prior programming experience is needed." who_for_1: "We recommend CodeCombat for students aged 9 and up. No prior programming experience is needed."
who_for_2: "We've designed CodeCombat to appeal to both boys and girls." who_for_2: "We've designed CodeCombat to appeal to both boys and girls."
material_title: "How much material is there?" material_title: "How much material is there?"
material_1: "Approximately 8 hours of free content, and an additional 14 hours of subscriber content." material_china: "Approximately 22 hours of gameplay spread over 120+ subscriber-only levels so far, with 5 new levels every week."
material_1: "Approximately 8 hours of free content and an additional 14 hours of subscriber content, with 5 new levels every week." # {change}
concepts_title: "What concepts are covered?" concepts_title: "What concepts are covered?"
how_much_title: "How much does a monthly subscription cost?" how_much_title: "How much does a monthly subscription cost?"
how_much_1: "A" how_much_1: "A"

View file

@ -304,6 +304,7 @@ _.extend UserSchema.properties,
siteref: { type: 'string' } siteref: { type: 'string' }
referrer: { type: 'string' } referrer: { type: 'string' }
chinaVersion: { type: 'boolean' }
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'

View file

@ -8,7 +8,7 @@ block header
a(href="/") a(href="/")
span.glyphicon.glyphicon-home span.glyphicon.glyphicon-home
a(href="/about", data-i18n="nav.about") a(href="/about", data-i18n="nav.about")
a(href='/play/ladder', data-i18n="home.multiplayer").multiplayer-nav-link //a(href='/play/ladder', data-i18n="home.multiplayer").multiplayer-nav-link
a(href='/community', data-i18n="nav.community") a(href='/community', data-i18n="nav.community")
a(href='http://blog.codecombat.com/', data-i18n="nav.blog") a(href='http://blog.codecombat.com/', data-i18n="nav.blog")
a(href='http://discourse.codecombat.com/', data-i18n="nav.forum") a(href='http://discourse.codecombat.com/', data-i18n="nav.forum")

View file

@ -78,8 +78,9 @@ block content
a(href="/contribute/artisan") a(href="/contribute/artisan")
img(src="/images/pages/community/artisan.png") img(src="/images/pages/community/artisan.png")
a(href="/contribute/adventurer") if !me.get('chinaVersion')
img(src="/images/pages/community/adventurer.png") a(href="/contribute/adventurer")
img(src="/images/pages/community/adventurer.png")
a(href="/contribute/scribe") a(href="/contribute/scribe")
img(src="/images/pages/community/scribe.png") img(src="/images/pages/community/scribe.png")

View file

@ -32,17 +32,18 @@ block content
p(data-i18n="classes.artisan_summary") p(data-i18n="classes.artisan_summary")
| Build and share levels for you and your friends to play. Become an Artisan to learn the art of teaching others to program. | Build and share levels for you and your friends to play. Become an Artisan to learn the art of teaching others to program.
a(href="/contribute/adventurer") if !me.get('chinaVersion')
div.class_tile a(href="/contribute/adventurer")
img.tile-img(src="/images/pages/contribute/tile_adventurer.png", alt="") div.class_tile
img.tile-img(src="/images/pages/contribute/tile_adventurer.png", alt="")
div.class_text
h3 div.class_text
span.spr(data-i18n="classes.adventurer_title") Adventurer h3
span(data-i18n="classes.adventurer_title_description") span.spr(data-i18n="classes.adventurer_title") Adventurer
span(data-i18n="classes.adventurer_title_description")
p(data-i18n="classes.adventurer_summary")
| Get our new levels (even our subscriber content) for free one week early and help us work out bugs before our public release. p(data-i18n="classes.adventurer_summary")
| Get our new levels (even our subscriber content) for free one week early and help us work out bugs before our public release.
a(href="/contribute/scribe") a(href="/contribute/scribe")
div.class_tile div.class_tile

View file

@ -19,8 +19,9 @@
table.table.table-condensed.table-bordered.comparison-table table.table.table-condensed.table-bordered.comparison-table
thead thead
tr tr
th th
th(data-i18n="subscribe.free") if !me.get('chinaVersion')
th(data-i18n="subscribe.free")
th th
//- TODO: find a better way to localize '$9.99/month' //- TODO: find a better way to localize '$9.99/month'
span $#{price}/ span $#{price}/
@ -29,38 +30,44 @@
tr tr
td.feature-description td.feature-description
span(data-i18n="subscribe.feature1") span(data-i18n="subscribe.feature1")
td.center-ok if !me.get('chinaVersion')
span.glyphicon.glyphicon-ok td.center-ok
span.glyphicon.glyphicon-ok
td.center-ok td.center-ok
span.glyphicon.glyphicon-ok span.glyphicon.glyphicon-ok
tr tr
td.feature-description td.feature-description
span(data-i18n="[html]subscribe.feature2") span(data-i18n="[html]subscribe.feature2")
td if !me.get('chinaVersion')
td
td.center-ok td.center-ok
span.glyphicon.glyphicon-ok span.glyphicon.glyphicon-ok
tr tr
td.feature-description td.feature-description
span(data-i18n="subscribe.feature3") span(data-i18n="subscribe.feature3")
td if !me.get('chinaVersion')
td
td.center-ok td.center-ok
span.glyphicon.glyphicon-ok span.glyphicon.glyphicon-ok
tr tr
td.feature-description td.feature-description
span(data-i18n="[html]subscribe.feature4") span(data-i18n="[html]subscribe.feature4")
td if !me.get('chinaVersion')
td
td.center-ok td.center-ok
span.glyphicon.glyphicon-ok span.glyphicon.glyphicon-ok
tr tr
td.feature-description td.feature-description
span(data-i18n="subscribe.feature5") span(data-i18n="subscribe.feature5")
td if !me.get('chinaVersion')
td
td.center-ok td.center-ok
span.glyphicon.glyphicon-ok span.glyphicon.glyphicon-ok
tr tr
td.feature-description td.feature-description
span(data-i18n="subscribe.feature6") span(data-i18n="subscribe.feature6")
td if !me.get('chinaVersion')
td
td.center-ok td.center-ok
span.glyphicon.glyphicon-ok span.glyphicon.glyphicon-ok
#parents-info(data-i18n="subscribe.parents") #parents-info(data-i18n="subscribe.parents")

View file

@ -49,10 +49,11 @@ block content
span.spl(data-i18n="legal.email_description_suffix") span.spl(data-i18n="legal.email_description_suffix")
| or through links in the emails we send, | or through links in the emails we send,
| you can change your preferences and easily unsubscribe at any time. | you can change your preferences and easily unsubscribe at any time.
h4(data-i18n="legal.cost_title") if !me.get('chinaVersion')
| Cost h4(data-i18n="legal.cost_title")
p(data-i18n="legal.cost_description") | Cost
| 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. p(data-i18n="legal.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.
h2(data-i18n="legal.copyrights_title") h2(data-i18n="legal.copyrights_title")
| Copyrights and Licenses | Copyrights and Licenses

View file

@ -10,7 +10,7 @@ if docs.length === 1
div div
!= docs[0].html != docs[0].html
if (me.get('preferredLanguage') || 'en-US').substr(0, 2) == 'en' if (!me.isPremium() || me.isAdmin()) && (me.get('preferredLanguage') || 'en-US').substr(0, 2) == 'en'
hr hr
h3 Want more programming lessons? h3 Want more programming lessons?
ul ul

View file

@ -11,8 +11,11 @@ block content
p(data-i18n="teachers.intro_2") p(data-i18n="teachers.intro_2")
h3(data-i18n="teachers.free_title") h3(data-i18n="teachers.free_title")
p(data-i18n="teachers.free_1") if me.get('chinaVersion')
p(data-i18n="teachers.free_2") p(data-i18n="teachers.cost_china")
else
p(data-i18n="teachers.free_1")
p(data-i18n="teachers.free_2")
h3.teachers-title(data-i18n="teachers.teacher_subs_title") h3.teachers-title(data-i18n="teachers.teacher_subs_title")
p p
@ -35,7 +38,10 @@ block content
p(data-i18n="teachers.who_for_2") p(data-i18n="teachers.who_for_2")
h3(data-i18n="teachers.material_title") h3(data-i18n="teachers.material_title")
p(data-i18n="teachers.material_1") if me.get('chinaVersion')
p(data-i18n="teachers.material_china")
else
p(data-i18n="teachers.material_1")
h3(data-i18n="teachers.concepts_title") h3(data-i18n="teachers.concepts_title")

View file

@ -53,7 +53,7 @@ module.exports = class InvoicesView extends RootView
amount: @amount amount: @amount
description: @description description: @description
bitcoin: true bitcoin: true
alipay: 'auto' alipay: if me.get('chinaVersion') or me.get('preferredLanguage')[...2] is 'zh' then true else 'auto'
onStripeReceivedToken: (e) -> onStripeReceivedToken: (e) ->
data = { data = {

View file

@ -247,7 +247,7 @@ class RecipientSubs
options = { options = {
description: "#{@recipientEmails.length} " + $.i18n.t('subscribe.stripe_description', defaultValue: 'Monthly Subscriptions') description: "#{@recipientEmails.length} " + $.i18n.t('subscribe.stripe_description', defaultValue: 'Monthly Subscriptions')
amount: amount amount: amount
alipay: 'auto' alipay: if me.get('chinaVersion') or me.get('preferredLanguage')[...2] is 'zh' then true else 'auto'
alipayReusable: true alipayReusable: true
} }
@state = 'start subscribe' @state = 'start subscribe'

View file

@ -112,7 +112,7 @@ module.exports = class SubscribeModal extends ModalView
options = { options = {
description: $.i18n.t('subscribe.stripe_description') description: $.i18n.t('subscribe.stripe_description')
amount: @product.amount amount: @product.amount
alipay: 'auto' alipay: if me.get('chinaVersion') or me.get('preferredLanguage')[...2] is 'zh' then true else 'auto'
alipayReusable: true alipayReusable: true
} }

View file

@ -432,7 +432,9 @@ module.exports = class CampaignView extends RootView
levelOriginal = levelElement.data('level-original') levelOriginal = levelElement.data('level-original')
level = _.find _.values(@campaign.get('levels')), slug: levelSlug level = _.find _.values(@campaign.get('levels')), slug: levelSlug
if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.slug] and not level.adventurer requiresSubscription = level.requiresSubscription or (me.get('chinaVersion') and not (level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'signs-and-portents', 'true-names']))
canPlayAnyway = not @requiresSubscription or level.adventurer
if requiresSubscription and not canPlayAnyway
@openModalView new SubscribeModal() @openModalView new SubscribeModal()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelSlug window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelSlug
else else

View file

@ -111,7 +111,7 @@ module.exports = class LevelLoadingView extends CocoView
onClickStartSubscription: (e) -> onClickStartSubscription: (e) ->
@openModalView new SubscribeModal() @openModalView new SubscribeModal()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'level loading', level: @options.level.get('slug') window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'level loading', level: @level?.get('slug') or @options.level?.get('slug')
onSubscribed: -> onSubscribed: ->
document.location.reload() document.location.reload()

View file

@ -70,7 +70,7 @@ module.exports = class BuyGemsModal extends ModalView
description: $.t(product.i18n) description: $.t(product.i18n)
amount: product.amount amount: product.amount
bitcoin: true bitcoin: true
alipay: 'auto' alipay: if me.get('chinaVersion') or me.get('preferredLanguage')[...2] is 'zh' then true else 'auto'
}) })
@productBeingPurchased = product @productBeingPurchased = product

View file

@ -0,0 +1,22 @@
var levelID = 'zero-sum';
var sessions = db.level.sessions.find({levelID: levelID, submitted: true}).toArray();
for (var i = 0; i < sessions.length; ++i) {
var session = sessions[i];
if (session.creatorName == 'The AI') continue;
if (!session.submittedCode) continue;
print("Unsubmitting " + session.creatorName + " " + session.team);
session.submitted = false;
db.level.sessions.save(session);
}
// // Resubmit
// var levelID = 'zero-sum';
// var sessions = db.level.sessions.find({levelID: levelID, submitted: false}).toArray();
// for (var i = 0; i < sessions.length; ++i) {
// var session = sessions[i];
// if (session.creatorName == 'The AI') continue;
// if (!session.submittedCode) continue;
// print("Resubmitting " + session.creatorName + " " + session.team);
// session.submitted = true; // false;
// db.level.sessions.save(session);
// }

View file

@ -82,6 +82,7 @@ LevelHandler = class LevelHandler extends Handler
super(arguments...) super(arguments...)
fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> fetchLevelByIDAndHandleErrors: (id, req, res, callback) ->
# TODO: this could probably be faster with projections, right?
@getDocumentForIdOrSlug id, (err, level) => @getDocumentForIdOrSlug id, (err, level) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless level? return @sendNotFoundError(res) unless level?
@ -103,7 +104,9 @@ LevelHandler = class LevelHandler extends Handler
Session.findOne(sessionQuery).exec (err, doc) => Session.findOne(sessionQuery).exec (err, doc) =>
return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) if err
return @sendSuccess(res, doc) if doc? return @sendSuccess(res, doc) if doc?
return @sendPaymentRequiredError(res, err) if (not req.user.isPremium()) and level.get('requiresSubscription') and not level.get('adventurer') requiresSubscription = level.get('requiresSubscription') or (req.user.get('chinaVersion') and level.get('campaign') and not (level.slug in ['dungeons-of-kithgard', 'gems-in-the-deep', 'shadow-guard', 'forgetful-gemsmith', 'signs-and-portents', 'true-names']))
canPlayAnyway = req.user.isPremium() or level.get 'adventurer'
return @sendPaymentRequiredError(res, err) if requiresSubscription and not canPlayAnyway
@createAndSaveNewSession sessionQuery, req, res @createAndSaveNewSession sessionQuery, req, res
createAndSaveNewSession: (sessionQuery, req, res) => createAndSaveNewSession: (sessionQuery, req, res) =>

View file

@ -202,3 +202,4 @@ module.exports.makeNewUser = makeNewUser = (req) ->
lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
user.set 'preferredLanguage', lang if lang[...2] isnt 'en' user.set 'preferredLanguage', lang if lang[...2] isnt 'en'
user.set 'lastIP', req.connection.remoteAddress user.set 'lastIP', req.connection.remoteAddress
user.set 'chinaVersion', true if req.chinaVersion

View file

@ -728,7 +728,7 @@ sendNextStepsEmail = (user, now, daysAgo) ->
possibleAdditionalOffers = ['code-school', 'one-month', 'learnable', 'pluralsight'] possibleAdditionalOffers = ['code-school', 'one-month', 'learnable', 'pluralsight']
for offer in _.sample possibleAdditionalOffers, nAdditionalOffers for offer in _.sample possibleAdditionalOffers, nAdditionalOffers
offers[offer] = true offers[offer] = true
# TODO: adjust template to not include any offers if user.isPremium()
# TODO: do something with the preferredLanguage? # TODO: do something with the preferredLanguage?
context = context =
email_id: sendwithus.templates.next_steps_email email_id: sendwithus.templates.next_steps_email

View file

@ -136,10 +136,11 @@ UserSchema.statics.updateServiceSettings = (doc, callback) ->
groupings: [{id: mail.MAILCHIMP_GROUP_ID, groups: newGroups}] groupings: [{id: mail.MAILCHIMP_GROUP_ID, groups: newGroups}]
'new-email': doc.get('email') 'new-email': doc.get('email')
} }
#params.merge_vars.chinaVersion = true if doc.get('chinaVersion') # ???
params.update_existing = true params.update_existing = true
onSuccess = (data) -> onSuccess = (data) ->
data.email = doc.get('email') # Make sure that we don't spam opt-in emails even if MailChimp doesn't udpate the email it gets in this object until they have confirmed. data.email = doc.get('email') # Make sure that we don't spam opt-in emails even if MailChimp doesn't update the email it gets in this object until they have confirmed.
doc.set('mailChimp', data) doc.set('mailChimp', data)
doc.updatedMailChimp = true doc.updatedMailChimp = true
doc.save() doc.save()
@ -291,7 +292,7 @@ UserSchema.statics.hashPassword = (password) ->
UserSchema.statics.privateProperties = [ UserSchema.statics.privateProperties = [
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID', 'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement', 'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID' 'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion'
] ]
UserSchema.statics.jsonSchema = jsonschema UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [ UserSchema.statics.editableProperties = [

View file

@ -79,19 +79,18 @@ setupPassportMiddleware = (app) ->
app.use(authentication.session()) app.use(authentication.session())
setupChinaRedirectMiddleware = (app) -> setupChinaRedirectMiddleware = (app) ->
isInChina = (req) -> shouldRedirectToChinaVersion = (req) ->
speaksChinese = req.acceptedLanguages[0]?.indexOf('zh') isnt -1
unless config.tokyo unless config.tokyo
ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress ip = req.headers['x-forwarded-for'] or req.connection.remoteAddress
geo = geoip.lookup(ip) geo = geoip.lookup(ip)
if geo?.country isnt "CN" then return false return geo?.country is "CN" and speaksChinese
firstAcceptedLanguage = req.acceptedLanguages[0]
isChinese = firstAcceptedLanguage?.indexOf? "zh"
return isChinese? and isChinese isnt -1
else else
return false #If the user is already redirected, don't redirect them! req.chinaVersion = true
return false # If the user is already redirected, don't redirect them!
app.use (req, res, next) -> app.use (req, res, next) ->
if isInChina req if shouldRedirectToChinaVersion req
res.writeHead 302, "Location": config.chinaDomain + req.url res.writeHead 302, "Location": config.chinaDomain + req.url
res.end() res.end()
else else