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
name: 'CodeCombat'
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) ->
Backbone.Mediator.publish 'stripe:received-token', { token: token }
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_2: "No experience required!"
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_2: "A monthly subscription provides access to video tutorials and extra practice levels."
teacher_subs_title: "Teachers get free subscriptions!"
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_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_3: "Video tutorials"
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_2: "We've designed CodeCombat to appeal to both boys and girls."
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?"
how_much_title: "How much does a monthly subscription cost?"
how_much_1: "A"

View file

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

View file

@ -8,7 +8,7 @@ block header
a(href="/")
span.glyphicon.glyphicon-home
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='http://blog.codecombat.com/', data-i18n="nav.blog")
a(href='http://discourse.codecombat.com/', data-i18n="nav.forum")

View file

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

View file

@ -32,17 +32,18 @@ block content
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.
a(href="/contribute/adventurer")
div.class_tile
img.tile-img(src="/images/pages/contribute/tile_adventurer.png", alt="")
div.class_text
h3
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.
if !me.get('chinaVersion')
a(href="/contribute/adventurer")
div.class_tile
img.tile-img(src="/images/pages/contribute/tile_adventurer.png", alt="")
div.class_text
h3
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.
a(href="/contribute/scribe")
div.class_tile

View file

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

View file

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

View file

@ -10,7 +10,7 @@ if docs.length === 1
div
!= 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
h3 Want more programming lessons?
ul

View file

@ -11,8 +11,11 @@ block content
p(data-i18n="teachers.intro_2")
h3(data-i18n="teachers.free_title")
p(data-i18n="teachers.free_1")
p(data-i18n="teachers.free_2")
if me.get('chinaVersion')
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")
p
@ -35,7 +38,10 @@ block content
p(data-i18n="teachers.who_for_2")
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")

View file

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

View file

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

View file

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

View file

@ -432,7 +432,9 @@ module.exports = class CampaignView extends RootView
levelOriginal = levelElement.data('level-original')
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()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelSlug
else

View file

@ -111,7 +111,7 @@ module.exports = class LevelLoadingView extends CocoView
onClickStartSubscription: (e) ->
@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: ->
document.location.reload()

View file

@ -70,7 +70,7 @@ module.exports = class BuyGemsModal extends ModalView
description: $.t(product.i18n)
amount: product.amount
bitcoin: true
alipay: 'auto'
alipay: if me.get('chinaVersion') or me.get('preferredLanguage')[...2] is 'zh' then true else 'auto'
})
@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...)
fetchLevelByIDAndHandleErrors: (id, req, res, callback) ->
# TODO: this could probably be faster with projections, right?
@getDocumentForIdOrSlug id, (err, level) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless level?
@ -103,7 +104,9 @@ 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.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) =>

View file

@ -202,3 +202,4 @@ module.exports.makeNewUser = makeNewUser = (req) ->
lang = languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
user.set 'preferredLanguage', lang if lang[...2] isnt 'en'
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']
for offer in _.sample possibleAdditionalOffers, nAdditionalOffers
offers[offer] = true
# TODO: adjust template to not include any offers if user.isPremium()
# TODO: do something with the preferredLanguage?
context =
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}]
'new-email': doc.get('email')
}
#params.merge_vars.chinaVersion = true if doc.get('chinaVersion') # ???
params.update_existing = true
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.updatedMailChimp = true
doc.save()
@ -291,7 +292,7 @@ UserSchema.statics.hashPassword = (password) ->
UserSchema.statics.privateProperties = [
'permissions', 'email', 'mailChimp', 'firstName', 'lastName', 'gender', 'facebookID',
'gplusID', 'music', 'volume', 'aceConfig', 'employerAt', 'signedEmployerAgreement',
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID'
'emailSubscriptions', 'emails', 'activity', 'stripe', 'stripeCustomerID', 'chinaVersion'
]
UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [

View file

@ -79,19 +79,18 @@ setupPassportMiddleware = (app) ->
app.use(authentication.session())
setupChinaRedirectMiddleware = (app) ->
isInChina = (req) ->
shouldRedirectToChinaVersion = (req) ->
speaksChinese = req.acceptedLanguages[0]?.indexOf('zh') isnt -1
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)
if geo?.country isnt "CN" then return false
firstAcceptedLanguage = req.acceptedLanguages[0]
isChinese = firstAcceptedLanguage?.indexOf? "zh"
return isChinese? and isChinese isnt -1
return geo?.country is "CN" and speaksChinese
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) ->
if isInChina req
if shouldRedirectToChinaVersion req
res.writeHead 302, "Location": config.chinaDomain + req.url
res.end()
else