mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Implementing alternative pricing with Alipay in China to support dedicated China server.
This commit is contained in:
parent
26b7c65855
commit
b4ea78e5cb
22 changed files with 100 additions and 53 deletions
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -304,6 +304,7 @@ _.extend UserSchema.properties,
|
|||
|
||||
siteref: { type: 'string' }
|
||||
referrer: { type: 'string' }
|
||||
chinaVersion: { type: 'boolean' }
|
||||
|
||||
c.extendBasicProperties UserSchema, 'user'
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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="")
|
||||
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")
|
||||
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.
|
||||
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
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
thead
|
||||
tr
|
||||
th
|
||||
th(data-i18n="subscribe.free")
|
||||
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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
22
scripts/mongodb/unsubmit-ladder.js
Normal file
22
scripts/mongodb/unsubmit-ladder.js
Normal 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);
|
||||
// }
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue