Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39550
1
##############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
$ = window.$
23
async = require('async')
24
misc = require('smc-util/misc')
25
_ = require('underscore')
26
27
{redux, rclass, React, ReactDOM, rtypes, Actions, Store} = require('./smc-react')
28
29
{Button, ButtonToolbar, FormControl, FormGroup, Row, Col, Accordion, Panel, Well, Alert, ButtonGroup, InputGroup} = require('react-bootstrap')
30
{ActivityDisplay, ErrorDisplay, Icon, Loading, SelectorInput, r_join, Space, TimeAgo, Tip, Footer} = require('./r_misc')
31
{HelpEmailLink, SiteName, PolicyPricingPageUrl, PolicyPrivacyPageUrl, PolicyCopyrightPageUrl} = require('./customize')
32
33
{PROJECT_UPGRADES} = require('smc-util/schema')
34
35
load_stripe = (cb) ->
36
if Stripe?
37
cb()
38
else
39
$.getScript("https://js.stripe.com/v2/").done(->cb()).fail(->cb('Unable to load Stripe support; make sure your browser is not blocking stripe.com.'))
40
41
last_subscription_attempt = null
42
43
actions = store = undefined
44
# Create the billing actions
45
class BillingActions extends Actions
46
clear_error: =>
47
@setState(error:'')
48
49
update_customer: (cb) =>
50
if @_update_customer_lock
51
return
52
@_update_customer_lock=true
53
@setState(action:"Updating billing information")
54
customer_is_defined = false
55
{webapp_client} = require('./webapp_client') # do not put at top level, since some code runs on server
56
async.series([
57
(cb) =>
58
webapp_client.stripe_get_customer
59
cb : (err, resp) =>
60
@_update_customer_lock = false
61
if not err and not resp?.stripe_publishable_key?
62
err = "WARNING: Stripe is not configured -- billing not available"
63
@setState(no_stripe:true)
64
if not err
65
@setState
66
customer : resp.customer
67
loaded : true
68
stripe_publishable_key : resp.stripe_publishable_key
69
customer_is_defined = resp.customer?
70
cb(err)
71
(cb) =>
72
if not customer_is_defined
73
cb()
74
else
75
# only call get_invoices if the customer already exists in the system!
76
webapp_client.stripe_get_invoices
77
limit : 100 # FUTURE: -- this will change when we use webhooks and our own database of info.
78
cb: (err, invoices) =>
79
if not err
80
@setState(invoices: invoices)
81
cb(err)
82
], (err) =>
83
@setState(error:err, action:'')
84
cb?(err)
85
)
86
87
88
_action: (action, desc, opts) =>
89
@setState(action: desc)
90
cb = opts.cb
91
opts.cb = (err) =>
92
@setState(action:'')
93
if err
94
@setState(error:err)
95
cb?(err)
96
else
97
@update_customer(cb)
98
{webapp_client} = require('./webapp_client') # do not put at top level, since some code runs on server
99
webapp_client["stripe_#{action}"](opts)
100
101
clear_action: =>
102
@setState(action:"", error:"")
103
104
delete_payment_method: (id, cb) =>
105
@_action('delete_source', 'Deleting a payment method', {card_id:id, cb:cb})
106
107
set_as_default_payment_method: (id, cb) =>
108
@_action('set_default_source', 'Setting payment method as default', {card_id:id, cb:cb})
109
110
submit_payment_method: (info, cb) =>
111
response = undefined
112
async.series([
113
(cb) =>
114
if not store.get("stripe_publishable_key")?
115
@update_customer(cb) # this defines stripe_publishable_key, or fails
116
else
117
cb()
118
(cb) =>
119
load_stripe(cb)
120
(cb) => # see https://stripe.com/docs/stripe.js#createToken
121
@setState(action:"Creating a new payment method -- get token from Stripe")
122
Stripe.setPublishableKey(store.get("stripe_publishable_key"))
123
Stripe.card.createToken info, (status, _response) =>
124
if status != 200
125
cb(_response.error.message)
126
else
127
response = _response
128
cb()
129
(cb) =>
130
@_action('create_source', 'Creating a new payment method (sending token)', {token:response.id, cb:cb})
131
], (err) =>
132
@setState(action:'', error:err)
133
cb?(err)
134
)
135
136
cancel_subscription: (id, cb) =>
137
@_action('cancel_subscription', 'Cancel a subscription', {subscription_id : id, cb : cb})
138
139
create_subscription: (plan='standard') =>
140
{webapp_client} = require('./webapp_client') # do not put at top level, since some code runs on server
141
lsa = last_subscription_attempt
142
if lsa? and lsa > misc.server_minutes_ago(2)
143
@setState(action:'', error: 'Too many subscription attempts in the last minute. Please **REFRESH YOUR BROWSER** THEN DOUBLE CHECK YOUR SUBSCRIPTION LIST!')
144
else
145
@setState(error: '')
146
@_action('create_subscription', 'Create a subscription', plan : plan)
147
last_subscription_attempt = misc.server_time()
148
149
# Cancel all subscriptions, remove credit cards, etc. -- this is not a normal action, and is used
150
# only when deleting an account. We allow it a callback.
151
cancel_everything: (cb) =>
152
async.series([
153
(cb) =>
154
# update info about this customer
155
@update_customer(cb)
156
(cb) =>
157
# delete stuff
158
async.parallel([
159
(cb) =>
160
# delete payment methods
161
ids = (x.id for x in redux.getStore('billing').getIn(['customer', 'sources', 'data'])?.toJS() ? [])
162
async.map(ids, @delete_payment_method, cb)
163
(cb) =>
164
# cancel subscriptions
165
ids = (x.id for x in redux.getStore('billing').getIn(['customer', 'subscriptions', 'data'])?.toJS() ? [] when not x.canceled_at)
166
async.map(ids, @cancel_subscription, cb)
167
], cb)
168
], cb)
169
170
171
actions = redux.createActions('billing', BillingActions)
172
store = redux.createStore('billing')
173
174
validate =
175
valid : {border:'1px solid green'}
176
invalid : {border:'1px solid red'}
177
178
powered_by_stripe = ->
179
<span>
180
Powered by <a href="https://stripe.com/" target="_blank" style={top: '7px', position: 'relative', fontSize: '23pt'}><Icon name='cc-stripe'/></a>
181
</span>
182
183
184
AddPaymentMethod = rclass
185
displayName : "AddPaymentMethod"
186
187
propTypes :
188
redux : rtypes.object.isRequired
189
on_close : rtypes.func.isRequired # called when this should be closed
190
191
getInitialState: ->
192
new_payment_info :
193
name : @props.redux.getStore('account').get_fullname()
194
number : ""
195
address_state : ""
196
address_country : ""
197
submitting : false
198
error : ''
199
cvc_help : false
200
201
submit_payment_method: ->
202
@setState(error: false, submitting:true)
203
@props.redux.getActions('billing').submit_payment_method @state.new_payment_info, (err) =>
204
@setState(error: err, submitting:false)
205
if not err
206
@props.on_close()
207
208
render_payment_method_field: (field, control) ->
209
if field == 'State' and @state.new_payment_info.address_country != "United States"
210
return
211
<Row key={field}>
212
<Col sm=4>
213
{field}
214
</Col>
215
<Col sm=8>
216
{control}
217
</Col>
218
</Row>
219
220
set_input_info: (field, ref, value) ->
221
x = misc.copy(@state.new_payment_info)
222
x[field] = value ? ReactDOM.findDOMNode(@refs[ref]).value
223
@setState(new_payment_info: x)
224
225
render_input_card_number: ->
226
icon = brand_to_icon($.payment.cardType(@state.new_payment_info.number))
227
value = if @valid('number') then $.payment.formatCardNumber(@state.new_payment_info.number) else @state.new_payment_info.number
228
<FormGroup>
229
<InputGroup>
230
<FormControl
231
autoFocus
232
ref = 'input_card_number'
233
style = @style('number')
234
type = 'text'
235
size = '20'
236
placeholder = '1234 5678 9012 3456'
237
value = {value}
238
onChange = {=>@set_input_info('number','input_card_number')}
239
disabled = {@state.submitting}
240
/>
241
<InputGroup.Addon>
242
<Icon name={icon} />
243
</InputGroup.Addon>
244
</InputGroup>
245
</FormGroup>
246
247
render_input_cvc_input: ->
248
<FormGroup>
249
<FormControl
250
ref = 'input_cvc'
251
style = {misc.merge({width:'5em'}, @style('cvc'))}
252
type = 'text'
253
size = 4
254
placeholder = '···'
255
onChange = {=>@set_input_info('cvc', 'input_cvc')}
256
disabled = {@state.submitting}
257
/>
258
</FormGroup>
259
260
render_input_cvc_help: ->
261
if @state.cvc_help
262
<div>The <a href='https://en.wikipedia.org/wiki/Card_security_code' target='_blank'>security code</a> is
263
located on the back of credit or debit cards and is a separate group of 3 (or 4) digits to the right of
264
the signature strip. <a href='' onClick={(e)=>e.preventDefault();@setState(cvc_help:false)}>(hide)</a></div>
265
else
266
<a href='' onClick={(e)=>e.preventDefault();@setState(cvc_help:true)}>(what is the security code?)</a>
267
268
render_input_cvc: ->
269
<Row>
270
<Col md=3>{@render_input_cvc_input()}</Col>
271
<Col md=9>{@render_input_cvc_help()}</Col>
272
</Row>
273
274
valid: (name) ->
275
info = @state.new_payment_info
276
277
if not name?
278
# check validity of all fields
279
for name in ['number','exp_month','exp_year','cvc','name', 'address_country']
280
if not @valid(name)
281
return false
282
if info.address_country == 'United States'
283
if not @valid('address_state') or not @valid('address_zip')
284
return false
285
return true
286
287
x = info[name]
288
if not x
289
return
290
switch name
291
when 'number'
292
return $.payment.validateCardNumber(x)
293
when 'exp_month'
294
if x.length == 0
295
return
296
month = parseInt(x)
297
return month >= 1 and month <= 12
298
when 'exp_year'
299
if x.length == 0
300
return
301
year = parseInt(x)
302
return year >= 15 and year <= 50
303
when 'cvc'
304
return $.payment.validateCardCVC(x)
305
when 'name'
306
return x.length > 0
307
when 'address_country'
308
return x.length > 0
309
when 'address_state'
310
return x.length > 0
311
when 'address_zip'
312
return misc.is_valid_zipcode(x)
313
314
style: (name) ->
315
a = @valid(name)
316
if not a?
317
return {}
318
else if a == true
319
return validate.valid
320
else
321
return validate.invalid
322
323
render_input_expiration: ->
324
<div style={marginBottom:'15px', display:'flex'}>
325
<FormGroup>
326
<FormControl
327
readOnly = {@state.submitting}
328
className = 'form-control'
329
style = {misc.merge({width:'5em'}, @style('exp_month'))}
330
placeholder = 'MM'
331
type = 'text'
332
size = '2'
333
onChange = {(e)=>@set_input_info('exp_month', undefined, e.target.value)}
334
/>
335
</FormGroup>
336
<span style={fontSize:'22px', margin: '1px 5px'}> / </span>
337
<FormGroup>
338
<FormControl
339
readOnly = {@state.submitting}
340
className = 'form-control'
341
style = {misc.merge({width:'5em'}, @style('exp_year'))}
342
placeholder = 'YY'
343
type = 'text'
344
size = '2'
345
onChange = {(e)=>@set_input_info('exp_year', undefined, e.target.value)}
346
/>
347
</FormGroup>
348
</div>
349
350
render_input_name: ->
351
<FormGroup>
352
<FormControl
353
ref = 'input_name'
354
type = 'text'
355
placeholder = 'Name on Card'
356
onChange = {=>@set_input_info('name', 'input_name')}
357
style = {@style('name')}
358
value = {@state.new_payment_info.name}
359
disabled = {@state.submitting}
360
/>
361
</FormGroup>
362
363
render_input_country: ->
364
<SelectorInput
365
options = {COUNTRIES}
366
on_change = {(country)=>@set_input_info('address_country', '', country)}
367
disabled = {@state.submitting}
368
/>
369
370
render_input_zip: ->
371
<FormGroup>
372
<FormControl
373
ref = 'input_address_zip'
374
style = {@style('address_zip')}
375
placeholder = 'Zip Code'
376
type = 'text'
377
size = '5'
378
pattern = '\d{5,5}(-\d{4,4})?'
379
onChange = {=>@set_input_info('address_zip', 'input_address_zip')}
380
disabled = {@state.submitting}
381
/>
382
</FormGroup>
383
384
render_tax_notice: ->
385
<Row>
386
<Col sm=12>
387
<Alert bsStyle='info'>
388
<h4><Icon name='exclamation-triangle' /> Notice </h4>
389
<p>Sales tax is applied in the state of Washington</p>
390
</Alert>
391
</Col>
392
</Row>
393
394
render_input_state_zip: ->
395
<div>
396
<Row>
397
<Col sm=7>
398
<SelectorInput
399
options = {STATES}
400
on_change = {(state)=>@set_input_info('address_state', '', state)}
401
disabled = {@state.submitting}
402
/>
403
</Col>
404
<Col sm=5>
405
{@render_input_zip()}
406
</Col>
407
</Row>
408
{@render_tax_notice() if @state.new_payment_info.address_state is 'WA'}
409
</div>
410
411
412
render_payment_method_fields: ->
413
PAYMENT_METHOD_FORM =
414
'Card Number' : @render_input_card_number
415
'Security Code (CVC)' : @render_input_cvc
416
'Expiration (MM/YY)' : @render_input_expiration
417
'Name on Card' : @render_input_name
418
'Country' : @render_input_country
419
'State' : @render_input_state_zip
420
421
for field, control of PAYMENT_METHOD_FORM
422
@render_payment_method_field(field, control())
423
424
render_payment_method_buttons: ->
425
<div>
426
<Row>
427
<Col sm=4>
428
{powered_by_stripe()}
429
</Col>
430
<Col sm=8>
431
<ButtonToolbar className='pull-right'>
432
<Button
433
onClick = {@submit_payment_method}
434
bsStyle = 'primary'
435
disabled = {not @valid() or @state.submitting}
436
>
437
Add Credit Card
438
</Button>
439
<Button onClick={@props.on_close}>Cancel</Button>
440
</ButtonToolbar>
441
</Col>
442
</Row>
443
<div style={color:"#666", marginTop:'15px'}>
444
(PayPal or wire transfers for non-recurring subscriptions above $50 are also possible. Please email <HelpEmailLink/>.)
445
</div>
446
</div>
447
448
render_error: ->
449
if @state.error
450
<ErrorDisplay error={@state.error} onClose={=>@setState(error:'')} />
451
452
render: ->
453
<Row>
454
<Col sm=6 smOffset=3>
455
<Well style={boxShadow:'5px 5px 5px lightgray', zIndex:2}>
456
{@render_error()}
457
{@render_payment_method_fields()}
458
{@render_payment_method_buttons()}
459
</Well>
460
</Col>
461
</Row>
462
463
464
#address_city: nulladdress_country: "United States"address_line1: nulladdress_line1_check: nulladdress_line2: nulladdress_state: "WA"address_zip: "98122"address_zip_check: "pass"brand: "Diners Club"country: nullcustomer: "cus_6TzOs3X3oawJxr"cvc_check: "pass"dynamic_last4: nullexp_month: 2exp_year: 2020fingerprint: "ukp9e1Ie0rPtwrXy"funding: "credit"id: "card_16MMxEGbwvoRbeYxoQoOUyno"last4: "5904"metadata: Object__proto__: Objectname: "William Stein"object: "card"tokenization_method: null__proto__: Object1: Objectlength: 2__proto__: Array[0]has_more: falseobject: "list"total_count: 2url: "/v1/customers/cus_6TzOs3X3oawJxr/sources"__proto__: Objectsubscriptions: Object__proto__: Object__proto__: Object
465
466
467
PaymentMethod = rclass
468
displayName : "PaymentMethod"
469
470
propTypes :
471
source : rtypes.object.isRequired
472
default : rtypes.bool # required for set_as_default
473
set_as_default : rtypes.func # called when this card should be set to default
474
delete_method : rtypes.func # called when this card should be deleted
475
476
getInitialState: ->
477
confirm_default : false
478
confirm_delete : false
479
480
icon_name: ->
481
return brand_to_icon(@props.source.brand.toLowerCase())
482
483
render_confirm_default: ->
484
<Alert bsStyle='warning'>
485
<Row>
486
<Col md=5 mdOffset=2>
487
<p>Are you sure you want to set this payment card to be the default?</p>
488
<p>All future payments will be made with the card that is the default <b>at the time of renewal</b>.
489
Changing your default card right before a subscription renewal will cause the <Space/>
490
new default to be charged instead of the previous one.</p>
491
</Col>
492
<Col md=5>
493
<ButtonToolbar>
494
<Button onClick={=>@setState(confirm_default:false)}>Cancel</Button>
495
<Button onClick={=>@setState(confirm_default:false);@props.set_as_default()} bsStyle='warning'>
496
<Icon name='trash'/> Set to Default
497
</Button>
498
</ButtonToolbar>
499
</Col>
500
</Row>
501
</Alert>
502
503
render_confirm_delete: ->
504
<Alert bsStyle='danger'>
505
<Row>
506
<Col md=5 mdOffset=2>
507
Are you sure you want to delete this payment method?
508
</Col>
509
<Col md=5>
510
<ButtonToolbar>
511
<Button onClick={=>@setState(confirm_delete:false)}>Cancel</Button>
512
<Button bsStyle='danger' onClick={=>@setState(confirm_delete:false);@props.delete_method()}>
513
<Icon name='trash'/> Delete Payment Method
514
</Button>
515
</ButtonToolbar>
516
</Col>
517
</Row>
518
</Alert>
519
520
render_card: ->
521
<Row>
522
<Col md=2>
523
<Icon name={@icon_name()} /> {@props.source.brand}
524
</Col>
525
<Col md=1>
526
<em···</em>{@props.source.last4}
527
</Col>
528
<Col md=1>
529
{@props.source.exp_month}/{@props.source.exp_year}
530
</Col>
531
<Col md=2>
532
{@props.source.name}
533
</Col>
534
<Col md=1>
535
{@props.source.country}
536
</Col>
537
<Col md=2>
538
{@props.source.address_state}
539
<Space/><Space/>
540
{@props.source.address_zip}
541
</Col>
542
{@render_action_buttons() if @props.set_as_default? or @props.delete_method?}
543
</Row>
544
545
render_action_buttons: ->
546
<Col md=3>
547
<ButtonToolbar style={float: "right"}>
548
{<Button
549
onClick = {=>@setState(confirm_default:true)}
550
disabled = {@props.default}
551
bsStyle = {if @props.default then 'primary' else 'default'}
552
>
553
Default{<span>... </span> if not @props.default}
554
</Button> if @props.set_as_default? }
555
556
{<Button onClick={=>@setState(confirm_delete:true)}>
557
<Icon name="trash" /> Delete
558
</Button> if @props.delete_method? }
559
</ButtonToolbar>
560
</Col>
561
562
render: ->
563
<div style={borderBottom:'1px solid #999', paddingTop: '5px', paddingBottom: '5px'}>
564
{@render_card()}
565
{@render_confirm_default() if @state.confirm_default}
566
{@render_confirm_delete() if @state.confirm_delete}
567
</div>
568
569
PaymentMethods = rclass
570
displayName : 'PaymentMethods'
571
572
propTypes :
573
redux : rtypes.object.isRequired
574
sources : rtypes.object.isRequired
575
default : rtypes.string
576
577
getInitialState: ->
578
state : 'view' # 'delete' <--> 'view' <--> 'add_new'
579
error : ''
580
581
add_payment_method: ->
582
@setState(state:'add_new')
583
584
render_add_payment_method: ->
585
if @state.state == 'add_new'
586
<AddPaymentMethod redux={@props.redux} on_close={=>@setState(state:'view')} />
587
588
render_add_payment_method_button: ->
589
<Button disabled={@state.state != 'view'} onClick={@add_payment_method} bsStyle='primary' className='pull-right'>
590
<Icon name='plus-circle' /> Add Payment Method...
591
</Button>
592
593
render_header: ->
594
<Row>
595
<Col sm=6>
596
<Icon name='credit-card' /> Payment Methods
597
</Col>
598
<Col sm=6>
599
{@render_add_payment_method_button()}
600
</Col>
601
</Row>
602
603
set_as_default: (id) ->
604
@props.redux.getActions('billing').set_as_default_payment_method(id)
605
606
delete_method: (id) ->
607
@props.redux.getActions('billing').delete_payment_method(id)
608
609
render_payment_method: (source) ->
610
<PaymentMethod
611
key = {source.id}
612
source = {source}
613
default = {source.id==@props.default}
614
set_as_default = {=>@set_as_default(source.id)}
615
delete_method = {=>@delete_method(source.id)}
616
/>
617
618
render_payment_methods: ->
619
for source in @props.sources.data
620
@render_payment_method(source)
621
622
render_error: ->
623
if @state.error
624
<ErrorDisplay error={@state.error} onClose={=>@setState(error:'')} />
625
626
render: ->
627
<Panel header={@render_header()}>
628
{@render_error()}
629
{@render_add_payment_method() if @state.state in ['add_new']}
630
{@render_payment_methods()}
631
</Panel>
632
633
exports.PaymentMethods = PaymentMethods
634
635
exports.ProjectQuotaBoundsTable = ProjectQuotaBoundsTable = rclass
636
render_project_quota: (name, value) ->
637
data = PROJECT_UPGRADES.params[name]
638
amount = value * data.pricing_factor
639
unit = data.pricing_unit
640
if unit == "day" and amount < 2
641
amount = 24 * amount
642
unit = "hour"
643
<div key={name} style={marginBottom:'5px', marginLeft:'10px'}>
644
<Tip title={data.display} tip={data.desc}>
645
<span style={fontWeight:'bold',color:'#666'}>
646
{misc.round1(amount)} {misc.plural(amount, unit)}
647
</span><Space/>
648
<span style={color:'#999'}>
649
{data.display}
650
</span>
651
</Tip>
652
</div>
653
654
render: ->
655
max = PROJECT_UPGRADES.max_per_project
656
<Panel
657
header = {<span>Maximum possible quotas <strong>per project</strong></span>}
658
>
659
{@render_project_quota(name, max[name]) for name in PROJECT_UPGRADES.field_order when max[name]}
660
</Panel>
661
662
exports.ProjectQuotaFreeTable = ProjectQuotaFreeTable = rclass
663
render_project_quota: (name, value) ->
664
# SMELL: is this a code dup from above?
665
data = PROJECT_UPGRADES.params[name]
666
amount = value * data.pricing_factor
667
unit = data.pricing_unit
668
if unit == "day" and amount < 2
669
amount = 24 * amount
670
unit = "hour"
671
<div key={name} style={marginBottom:'5px', marginLeft:'10px'}>
672
<Tip title={data.display} tip={data.desc}>
673
<span style={fontWeight:'bold',color:'#666'}>
674
{misc.round1(amount)} {misc.plural(amount, unit)}
675
</span> <Space/>
676
<span style={color:'#999'}>
677
{data.display}
678
</span>
679
</Tip>
680
</div>
681
682
render_header: ->
683
<div style={paddingLeft:"10px"}>
684
<Icon name='battery-empty' />{' '}
685
<span style={fontWeight:'bold'}>Free plan</span>
686
</div>
687
688
render: ->
689
free = require('smc-util/schema').DEFAULT_QUOTAS
690
<Panel
691
header = {@render_header()}
692
bsStyle = 'info'
693
>
694
<Space/>
695
<div style={marginBottom:'5px', marginLeft:'10px'}>
696
<Tip title="Free servers" tip="Many free projects are cramped together inside weaker compute machines, competing for CPU, RAM and I/O.">
697
<span style={fontWeight:'bold',color:'#666'}>low-grade</span><Space/>
698
<span style={color:'#999'}>Server hosting</span>
699
</Tip>
700
</div>
701
<div style={marginBottom:'5px', marginLeft:'10px'}>
702
<Tip title="Internet access" tip="Despite working inside a web-browser, free projects are not allowed to directly access the internet due to security/abuse reasons.">
703
<span style={fontWeight:'bold',color:'#666'}>no</span><Space/>
704
<span style={color:'#999'}>Internet access</span>
705
</Tip>
706
</div>
707
{@render_project_quota(name, free[name]) for name in PROJECT_UPGRADES.field_order when free[name]}
708
<Space/>
709
<div style={textAlign : 'center', marginTop:'10px'}>
710
<h3 style={textAlign:'left'}>
711
<span style={fontSize:'16px', verticalAlign:'super'}>$</span><Space/>
712
<span style={fontSize:'30px'}>0</span>
713
</h3>
714
</div>
715
</Panel>
716
717
PlanInfo = rclass
718
displayName : 'PlanInfo'
719
720
propTypes :
721
plan : rtypes.string.isRequired
722
period : rtypes.string.isRequired # 'month', 'year', or 'month year'
723
selected : rtypes.bool
724
on_click : rtypes.func
725
726
getDefaultProps: ->
727
selected : false
728
729
render_plan_info_line: (name, value, data) ->
730
<div key={name} style={marginBottom:'5px', marginLeft:'10px'}>
731
<Tip title={data.display} tip={data.desc}>
732
<span style={fontWeight:'bold',color:'#444'}>
733
{value * data.pricing_factor} {misc.plural(value * data.pricing_factor, data.pricing_unit)}
734
</span>
735
<Space/>
736
<span style={color:'#666'}>
737
{data.display}
738
</span>
739
</Tip>
740
</div>
741
742
render_cost: (price, period) ->
743
period = PROJECT_UPGRADES.period_names[period] ? period
744
<span key={period} style={whiteSpace:'nowrap'}>
745
<span style={fontSize:'16px', verticalAlign:'super'}>$</span><Space/>
746
<span style={fontSize:'30px'}>{price}</span>
747
<span style={fontSize:'14px'}> / {period}</span>
748
</span>
749
750
render_price: (prices, periods) ->
751
if @props.on_click?
752
# note: in non-static, there is always just *one* price (several only on "static" pages)
753
for i in [0...prices.length]
754
<Button key={i} bsStyle={if @props.selected then 'primary'}>
755
{@render_cost(prices[i], periods[i])}
756
</Button>
757
else
758
<h3 style={textAlign:'left'}>
759
{r_join((@render_cost(prices[i], periods[i]) for i in [0...prices.length]), <br/>)}
760
</h3>
761
762
render_plan_name: (plan_data) ->
763
<div style={paddingLeft:"10px"}>
764
<Icon name={plan_data.icon} /> <span style={fontWeight:'bold'}>{misc.capitalize(@props.plan).replace(/_/g,' ')} plan</span>
765
</div>
766
767
render: ->
768
plan_data = PROJECT_UPGRADES.membership[@props.plan]
769
if not plan_data?
770
return <div>Unknown plan type: {@props.plan}</div>
771
772
params = PROJECT_UPGRADES.params
773
periods = misc.split(@props.period)
774
prices = (plan_data.price[period] for period in periods)
775
benefits = plan_data.benefits
776
777
style =
778
cursor : if @props.on_click? then 'pointer'
779
780
<Panel
781
style = {style}
782
className = 'smc-grow'
783
header = {@render_plan_name(plan_data)}
784
bsStyle = {if @props.selected then 'primary' else 'info'}
785
onClick = {=>@props.on_click?()}
786
>
787
<Space/>
788
{@render_plan_info_line(name, benefits[name] ? 0, params[name]) for name in PROJECT_UPGRADES.field_order when benefits[name]}
789
<Space/>
790
791
<div style={textAlign : 'center', marginTop:'10px'}>
792
{@render_price(prices, periods)}
793
</div>
794
795
</Panel>
796
797
AddSubscription = rclass
798
displayName : 'AddSubscription'
799
800
propTypes :
801
on_close : rtypes.func.isRequired
802
selected_plan : rtypes.string
803
actions : rtypes.object.isRequired
804
805
getDefaultProps: ->
806
selected_plan : ''
807
808
getInitialState: ->
809
selected_button : 'month'
810
811
is_recurring: ->
812
not PROJECT_UPGRADES.membership[@props.selected_plan.split('-')[0]].cancel_at_period_end
813
814
submit_create_subscription: ->
815
plan = @props.selected_plan
816
@props.actions.create_subscription(plan)
817
818
set_button_and_deselect_plans: (button) ->
819
if @state.selected_button isnt button
820
set_selected_plan('')
821
@setState(selected_button : button)
822
823
render_period_selection_buttons: ->
824
<div>
825
<ButtonGroup bsSize='large' style={marginBottom:'20px', display:'flex'}>
826
<Button
827
bsStyle = {if @state.selected_button is 'month' then 'primary'}
828
onClick = {=>@set_button_and_deselect_plans('month')}
829
>
830
Monthly subscriptions
831
</Button>
832
<Button
833
bsStyle = {if @state.selected_button is 'year' then 'primary'}
834
onClick = {=>@set_button_and_deselect_plans('year')}
835
>
836
Yearly subscriptions
837
</Button>
838
<Button
839
bsStyle = {if @state.selected_button is 'month4' then 'primary'}
840
onClick = {=>@set_button_and_deselect_plans('month4')}
841
>
842
4-Month course packages
843
</Button>
844
<Button
845
bsStyle = {if @state.selected_button is 'year1' then 'primary'}
846
onClick = {=>@set_button_and_deselect_plans('year1')}
847
>
848
Yearly course packages
849
</Button>
850
</ButtonGroup>
851
</div>
852
853
render_renewal_info: ->
854
if @props.selected_plan
855
renews = not PROJECT_UPGRADES.membership[@props.selected_plan.split('-')[0]].cancel_at_period_end
856
length = PROJECT_UPGRADES.period_names[@state.selected_button]
857
<p style={marginBottom:'1ex', marginTop:'1ex'}>
858
{<span>This subscription will <b>automatically renew</b> every {length}. You can cancel automatic renewal at any time.</span> if renews}
859
{<span>You will be <b>charged only once</b> for the course package, which lasts {length}. It does <b>not automatically renew</b>.</span> if not renews}
860
</p>
861
862
render_subscription_grid: ->
863
<SubscriptionGrid period={@state.selected_button} selected_plan={@props.selected_plan} />
864
865
render_dedicated_resources: ->
866
<div style={marginBottom:'15px'}>
867
<ExplainResources type='dedicated'/>
868
</div>
869
870
render_create_subscription_options: ->
871
# <h3><Icon name='list-alt'/> Sign up for a Subscription</h3>
872
<div>
873
<div style={textAlign:'center'}>
874
{@render_period_selection_buttons()}
875
</div>
876
{@render_subscription_grid()}
877
</div>
878
###
879
if @state.selected_button is 'month' or @state.selected_button is 'year'}
880
{@render_dedicated_resources() if @state.selected_button is 'dedicated_resources'}
881
###
882
883
render_create_subscription_confirm: ->
884
if @is_recurring()
885
subscription = " and you will be signed up for a recurring subscription"
886
<Alert>
887
<h4><Icon name='check' /> Confirm your selection </h4>
888
<p>You have selected the <span style={fontWeight:'bold'}>{misc.capitalize(@props.selected_plan).replace(/_/g,' ')} subscription</span>.</p>
889
{@render_renewal_info()}
890
<p>By clicking 'Add Subscription' your payment card will be immediately charged{subscription}.</p>
891
</Alert>
892
893
render_create_subscription_buttons: ->
894
<Row>
895
<Col sm=4>
896
{powered_by_stripe()}
897
</Col>
898
<Col sm=8>
899
<ButtonToolbar className='pull-right'>
900
<Button
901
bsStyle = 'primary'
902
onClick = {=>(@submit_create_subscription();@props.on_close())}
903
disabled = {@props.selected_plan is ''} >
904
<Icon name='check' /> Add Subscription
905
</Button>
906
<Button onClick={@props.on_close}>
907
Cancel
908
</Button>
909
</ButtonToolbar>
910
</Col>
911
</Row>
912
913
render: ->
914
<Row>
915
<Col sm=10 smOffset=1>
916
<Well style={boxShadow:'5px 5px 5px lightgray', zIndex:1}>
917
{@render_create_subscription_options()}
918
{@render_create_subscription_confirm() if @props.selected_plan isnt ''}
919
{<ConfirmPaymentMethod
920
is_recurring = {@is_recurring()}
921
/> if @props.selected_plan isnt ''}
922
{@render_create_subscription_buttons()}
923
</Well>
924
<ExplainResources type='shared'/>
925
</Col>
926
</Row>
927
928
ConfirmPaymentMethod = rclass
929
reduxProps :
930
billing :
931
customer : rtypes.object
932
933
propTypes :
934
is_recurring : rtypes.bool
935
936
render_single_payment_confirmation: ->
937
<span>
938
<p>Payment will be processed with the card below.</p>
939
<p>To change payment methods, please change your default card above.</p>
940
</span>
941
942
943
render_recurring_payment_confirmation: ->
944
<span>
945
<p>The initial payment will be processed with the card below.</p>
946
<p>Future payments will be made with your default card<Space/>
947
<b>at the time of renewal</b>.
948
Changing your default card right before renewal will cause the<Space/>
949
new default to be charged instead of the previous one.</p>
950
</span>
951
952
render: ->
953
for card_data in @props.customer.sources.data
954
if card_data.id == @props.customer.default_source
955
default_card = card_data
956
957
<Alert>
958
<h4><Icon name='check' /> Confirm your payment card</h4>
959
{@render_single_payment_confirmation() if not @props.is_recurring}
960
{@render_recurring_payment_confirmation() if @props.is_recurring}
961
<Well>
962
<PaymentMethod
963
source = {default_card}
964
/>
965
</Well>
966
</Alert>
967
968
969
exports.SubscriptionGrid = SubscriptionGrid = rclass
970
displayName : 'SubscriptionGrid'
971
972
propTypes :
973
period : rtypes.string.isRequired # see docs for PlanInfo
974
selected_plan : rtypes.string
975
is_static : rtypes.bool # used for display mode
976
977
getDefaultProps: ->
978
is_static : false
979
980
is_selected: (plan, period) ->
981
if @props.period is 'year'
982
return @props.selected_plan is "#{plan}-year"
983
else
984
return @props.selected_plan is plan
985
986
render_plan_info: (plan, period) ->
987
<PlanInfo
988
plan = {plan}
989
period = {period}
990
selected = {@is_selected(plan, period)}
991
on_click = {if not @props.is_static then ->set_selected_plan(plan, period)} />
992
993
render_cols: (row, ncols) ->
994
width = 12/ncols
995
for plan in row
996
<Col sm={width} key={plan}>
997
{@render_plan_info(plan, @props.period)}
998
</Col>
999
1000
render_rows: (live_subscriptions, ncols) ->
1001
for i, row of live_subscriptions
1002
<Row key={i}>
1003
{@render_cols(row, ncols)}
1004
</Row>
1005
1006
render: ->
1007
live_subscriptions = []
1008
periods = misc.split(@props.period)
1009
for row in PROJECT_UPGRADES.live_subscriptions
1010
v = []
1011
for x in row
1012
price_keys = _.keys(PROJECT_UPGRADES.membership[x].price)
1013
if _.intersection(periods, price_keys).length > 0
1014
v.push(x)
1015
if v.length > 0
1016
live_subscriptions.push(v)
1017
# Compute the maximum number of columns in any row
1018
ncols = Math.max((row.length for row in live_subscriptions)...)
1019
# Round up to nearest divisor of 12
1020
if ncols == 5
1021
ncols = 6
1022
else if ncols >= 7
1023
ncols = 12
1024
<div>
1025
{@render_rows(live_subscriptions, ncols)}
1026
</div>
1027
1028
1029
exports.ExplainResources = ExplainResources = rclass
1030
propTypes :
1031
type : rtypes.string.isRequired # 'shared', 'dedicated'
1032
is_static : rtypes.bool
1033
1034
getDefaultProps: ->
1035
is_static : false
1036
1037
render_shared: ->
1038
<div>
1039
<Row>
1040
<Col md=8 sm=12>
1041
<a name="projects"></a>
1042
<h4>Projects</h4>
1043
<div>
1044
Your work on <SiteName/> happens inside <em>projects</em>.
1045
You may create any number of independent projects.
1046
They form your personal workspaces,
1047
where you privately store your files, computational worksheets, and data.
1048
You typically run computations through the web-interface,
1049
either via a worksheet, notebook, or by executing a program in a terminal
1050
(you can also ssh into any project).
1051
You can also invite collaborators to work with you inside a project,
1052
and you can explicitly make files or directories publicly available
1053
to everybody.
1054
</div>
1055
<Space/>
1056
1057
<h4>Shared Resources</h4>
1058
<div>
1059
Each projects runs on a server, where it shares disk space, CPU, and RAM with other projects.
1060
Initially, projects run with default free quotas on heavily used free machines that are rebooted frequently.
1061
You can upgrade any quota on any project on which you collaborate, and you can move projects
1062
to faster very stable <em>members-only computers</em>,
1063
where there is much less competition for resources.
1064
If a project on a free computer is not used for a few weeks, it gets moved to secondary storage, and
1065
starting it up will take longer; in contrast, projects on members-only computers always start
1066
up very quickly.
1067
</div>
1068
<Space/>
1069
1070
<h4>Quota upgrades</h4>
1071
<div>
1072
By purchasing one or more of our subscriptions,
1073
you receive a certain amount of <em>quota upgrades</em>.
1074
<ul style={paddingLeft:"20px"}>
1075
<li>You can upgrade the quotas on any of your projects
1076
up to the total amount given by your subscription(s)
1077
and the upper limits per project.
1078
</li>
1079
<li>Project collaborators can collectively contribute to the same project,
1080
in order to increase the quotas of their common project
1081
&mdash; these contributions benefit all project collaborators.</li>
1082
<li>You can remove your contributions to any project (owner or collaborator) at any time.</li>
1083
<li>You may also subscribe to the same subscription more than once,
1084
in order to increase your total amount of quota upgrades.</li>
1085
</ul>
1086
</div>
1087
<Space/>
1088
1089
<div style={fontWeight:"bold"}>
1090
Please immediately email us at <HelpEmailLink/> {" "}
1091
{if not @props.is_static then <span> or read our <a target='_blank' href="#{PolicyPricingPageUrl}#faq">pricing FAQ</a> </span>}
1092
if anything is unclear to you.
1093
</div>
1094
<Space/>
1095
</Col>
1096
<Col md=4 sm=12>
1097
<Row>
1098
<Col md=12 sm=6>
1099
<ProjectQuotaFreeTable/>
1100
</Col>
1101
<Col md=12 sm=6>
1102
<ProjectQuotaBoundsTable/>
1103
</Col>
1104
</Row>
1105
</Col>
1106
</Row>
1107
</div>
1108
1109
render_dedicated: ->
1110
<div>
1111
<h4>Dedicated Resources</h4>
1112
You may also rent dedicated computers.
1113
Projects on such a machine of your choice get full use of the hard disk, CPU and RAM,
1114
and do <em>not</em> have to compete with other users for resources.
1115
We have not fully automated purchase of dedicated computers yet,
1116
so please contact us at <HelpEmailLink/> if you need a dedicated machine.
1117
</div>
1118
1119
render: ->
1120
switch @props.type
1121
when 'shared'
1122
return @render_shared()
1123
when 'dedicated'
1124
return @render_dedicated()
1125
else
1126
throw Error("unknown type #{@props.type}")
1127
1128
exports.ExplainPlan = ExplainPlan = rclass
1129
propTypes :
1130
type : rtypes.string.isRequired # 'personal', 'course'
1131
1132
render_personal: ->
1133
<div style={marginBottom:"10px"}>
1134
<h3>Personal subscriptions</h3>
1135
<div>
1136
We offer several subscriptions that let you upgrade the default free quotas on projects.
1137
You can distribute these upgrades to your own projects or any projects where you are a collaborator &mdash;
1138
everyone participating in such a collective project benefits and can easily change their allocations at any time!
1139
You can get higher-quality hosting on members-only machines and enable access to the internet from projects.
1140
You can also increase quotas for CPU and RAM, so that you can work on larger problems and
1141
do more computations simultaneously.
1142
</div>
1143
</div>
1144
1145
render_course: ->
1146
<div style={marginBottom:"10px"}>
1147
<h3>Course packages</h3>
1148
<div>
1149
<p>
1150
We offer course packages to support teaching using <SiteName/>.
1151
They start right after purchase and last for the indicated period and do <b>not auto-renew</b>.
1152
Following <a href="https://tutorial.cocalc.com/" target="_blank">this
1153
guide</a>, create a course file.
1154
Each time you add a student to your course, a project will be automatically created for that student.
1155
You can create and distribute assignments,
1156
students work on assignments inside their project (where you can see their progress
1157
in realtime and answer their questions),
1158
and you later collect and grade their assignments, then return them.
1159
</p>
1160
1161
<p>
1162
Paying is optional, but will ensure that your students have a better
1163
experience, network access, and receive priority support. The cost
1164
is <b>between $4 and $9 per student</b>, depending on class size and whether
1165
you or your students pay. <b>Start right now:</b> <i>you can fully setup your class
1166
and add students immediately before you pay us anything!</i>
1167
1168
</p>
1169
1170
<h4>Your or your institution pays</h4>
1171
You or your institution may pay for one of the course plans. You then use your plan to upgrade
1172
all projects in the course in the settings tab of the course file.
1173
1174
<h4>Students pay</h4>
1175
In the settings tab of your course, you require that all students
1176
pay a one-time $9 fee to move their
1177
projects to members only hosts and enable full internet access.
1178
1179
<br/>
1180
1181
<br/>
1182
1183
</div>
1184
</div>
1185
1186
render: ->
1187
switch @props.type
1188
when 'personal'
1189
return @render_personal()
1190
when 'course'
1191
return @render_course()
1192
else
1193
throw Error("unknown plan type #{@props.type}")
1194
1195
# ~~~ FAQ START
1196
1197
# some variables used in the text below
1198
faq_course_120 = 2 * PROJECT_UPGRADES.membership.medium_course.benefits.member_host
1199
faq_academic_students = PROJECT_UPGRADES.membership.small_course.benefits.member_host
1200
faq_academic_nb_standard = Math.ceil(faq_academic_students / PROJECT_UPGRADES.membership.standard.benefits.member_host)
1201
faq_academic_full = faq_academic_nb_standard * 4 * PROJECT_UPGRADES.membership.standard.price.month
1202
faq_idle_time_free_h = require('smc-util/schema').DEFAULT_QUOTAS.mintime / 60 / 60
1203
1204
# the structured react.js FAQ text
1205
FAQS =
1206
differences:
1207
q: <span>What is the difference between <b>free and paid plans</b>?</span>
1208
a: <span>The main differences are increased quotas and the quality of hosting; we also
1209
prioritize supporting paying users.
1210
We very strongly encourage you to make an account and explore our product for free!
1211
There is no difference in functionality between the free and for-pay versions of
1212
<SiteName/>; everything is still private by default for free users, and you can
1213
make as many projects as you want. You can even fully start teaching a course
1214
in <SiteName/> completely for free, then upgrade at any point later so that your students
1215
have a <b>much</b> better quality experience (for a small fraction of the cost of
1216
their textbook).
1217
</span>
1218
member_hosting:
1219
q: <span>What does <b>"member hosting"</b> mean?</span>
1220
a: <span>
1221
There are two types of projects: "free projects" and "member projects".
1222
Free projects run on heavily loaded computers.
1223
Quite often, these computers will house over 150 simultaneously running projects!
1224
Member-hosted projects are moved to much less loaded machine,
1225
which are reserved only for paying customers.<br/>
1226
Working in member-hosted projects feels much smoother because commands execute
1227
more quickly with lower latency,
1228
and CPU, memory and I/O heavy operations run more quickly.
1229
Additionally, members only projects are always "ready to start".
1230
Free projects that are not used for a few weeks are moved to "cold storage",
1231
and it can take a while to move them back onto a free machine when you
1232
later start them.
1233
</span>
1234
network_access:
1235
q: <span>What exactly does the quota <b>"internet access"</b> mean?</span>
1236
a: <span>
1237
Despite the fact that you are accessing <SiteName/> through the internet,
1238
you are actually working in a highly restricted environment.
1239
Processes running <em>inside</em> a free project are not allowed to directly
1240
access the internet. (We do not allow such access for free users, since when we did,
1241
malicious users launched attacks on other computers from <SiteName/>.)
1242
Enable internet access by adding the "internet access" quota.
1243
</span>
1244
idle_timeout:
1245
q: <span>What exactly does the quota <b>"idle timeout"</b> mean?</span>
1246
a: <span>
1247
By default, free projects stop running after {faq_idle_time_free_h} hour of idle time.
1248
This makes doing an overnight research computation &mdash;
1249
e.g., searching for special prime numbers &mdash; impossible.
1250
With an increased idle timeout, projects are allowed to run longer unattended.
1251
Processes might still stop if they use too much memory, crash due to an exception, or if the server they are
1252
running on is rebooted.
1253
(NOTE: Projects do not normally stop if you are continuously using them, and there are no
1254
daily or monthly caps on how much you may use a <SiteName/> project, even a free one.)
1255
</span>
1256
cpu_shares:
1257
q: <span>What are <b>"CPU shares"</b> and <b>"CPU cores"</b>?</span>
1258
a: <span>
1259
All projects on a single server share the underlying resources.
1260
These quotas determine how CPU resources are shared between projects.
1261
Increasing them increases the priority of a project compared to others on the same host computer.<br/>
1262
In particular, "shares" determines the amount of relative CPU time you get.
1263
</span>
1264
course120:
1265
q: <span>
1266
I have a <b>course of {faq_course_120 - 20} students</b>.
1267
Which plan should I purchase?
1268
</span>
1269
a: <span>
1270
You can combine and add up course subscriptions!
1271
By ordering two times the 'medium course plan',
1272
you will get {faq_course_120} upgrades covering all your students.
1273
</span>
1274
academic:
1275
q: <span>Do you offer <b>academic discounts</b>?</span>
1276
a: <span>
1277
Our course subscriptions are for academic use, and are already significantly discounted from the standard plans.
1278
Please compare our monthly plans with the 4 month course plans.
1279
For example, giving {faq_academic_students} students better member hosting and internet access
1280
would require subscribing to {faq_academic_nb_standard} "standard plans" for 4 months
1281
amounting to ${faq_academic_full}.
1282
</span>
1283
academic_quotas:
1284
q: <span>There are no CPU/RAM upgrades for courses. Is this enough?</span>
1285
a: <span>
1286
From our experience, we have found that for the type of computations used in most courses,
1287
the free quotas for memory and disk space are plenty.
1288
We do strongly suggest the classes upgrade all projects to "members-only" hosting,
1289
since this provides much better computers with higher availability.
1290
</span>
1291
invoice:
1292
q: <span>How do I get an <b>invoice</b> with a specific information?</span>
1293
a: <span>
1294
After purchasing, please email us at <HelpEmailLink />, reference what you bought,
1295
and tell us the payer{"'"}s name, contact information and any other specific
1296
instructions. We will then respond with a custom invoice for your purchase that
1297
satisfies your unique requirements.
1298
</span>
1299
course_required_plan:
1300
q: <span>Am I <strong>required to pay</strong> for conducting a course?</span>
1301
a: <span>
1302
<strong>No.</strong> You can use all course related functionalities under a free plan.
1303
</span>
1304
student_files:
1305
q: <span>What happens with the <strong>files of my students</strong> after the course finishes?</span>
1306
a: <span>
1307
Students will <strong>continue to have access</strong> to their files after the course,
1308
regardless of running the course under a paid plan or for free.
1309
Their projects remain accessible,
1310
they can (optionally) upgrade their projects with their own subscriptions,
1311
and they can also download all files to their local computer.
1312
</span>
1313
close_browser:
1314
q: <span>Can I <b>close my web-browser</b> while I{"'"}m working?</span>
1315
a: <span>
1316
<b>Yes!</b> When you close your web-browser, all your processes and running sessions continue running.
1317
You can start a computation, shut down your computer, go somewhere else, sign in
1318
on another computer, and continue working where you left off.
1319
(Note that output from Jupyter notebook computations will be lost, though Sage worksheet output is
1320
properly captured.)
1321
<br/>
1322
The only reasons why a project or process stops are
1323
that it hits its <em>idle timeout</em>, has used too much memory,
1324
crashed due to an exception, or the server had to reboot.
1325
</span>
1326
private:
1327
q: <span>Which plan offers <b>"private" file storage</b>?</span>
1328
a: <span>All our plans (free and paid) host your files privately by default.
1329
Please read our <a target="_blank" href=PolicyPrivacyPageUrl>Privacy Policy</a> and {" "}
1330
<a target="_blank" href=PolicyCopyrightPageUrl>Copyright Notice</a>.
1331
</span>
1332
git:
1333
q: <span>Can I work with <b>Git</b> &mdash; including GitHub, Bitbucket, GitLab, etc.?</span>
1334
a: <span>
1335
Git and various other source control tools are installed and ready to use via the "Terminal".
1336
But, in order to also interoperate with sites hosting Git repositories,
1337
you have to purchase a plan giving you "internet upgrades" and then applying this upgrade to your project.
1338
</span>
1339
backups:
1340
q: <span>Are my files backed up?</span>
1341
a: <span>
1342
All files in every project are snapshotted every 5 minutes. You can browse your snapshots by
1343
clicking the <b>"Backups"</b> link to the right of the file listing. Also, <SiteName/> records
1344
the history of all edits you or your collaborators make to most files, and you can browse
1345
that history with a slider by clicking on the "History" button (next to save) in files.
1346
We care about your data, and also make offsite backups periodically to encrypted USB
1347
drives that are not physically connected to the internet.
1348
</span>
1349
download_everything:
1350
q: <span>How can I <strong>download my project files</strong>?</span>
1351
a: <ol>
1352
<li>
1353
You can download each file individually via the "Files" interface.
1354
Select the file and click the "Download" button.
1355
</li>
1356
<li>
1357
It is also possible to create an archive for a directory or all files.
1358
For that, create a "Terminal"-file and issue one of these commands:
1359
<ul>
1360
<li>
1361
ZIP archive (Windows): <code>zip -r9 "[filename].zip" [directory-name...]</code>
1362
</li>
1363
<li>
1364
Tarball (Unix-like): <code>tar cjvf "[filename].tar.bz2" [directory-name...]</code>
1365
</li>
1366
</ul>
1367
(Replace <code>[filename]</code> with the actual filename and <code>[directory-name]</code> by one or more filenames or directory names.)
1368
Afterwards, download the archive as explained above.
1369
</li>
1370
</ol>
1371
1372
1373
FAQ = exports.FAQ = rclass
1374
displayName : 'FAQ'
1375
1376
faq: ->
1377
for qid, qa of FAQS
1378
<li key={qid} style={marginBottom:"10px"}>
1379
<em style={fontSize:"120%"}>{qa.q}</em>
1380
<br/>
1381
<span>{qa.a}</span>
1382
</li>
1383
1384
render: ->
1385
<div>
1386
<a name="faq"></a>
1387
<h2>Frequently asked questions</h2>
1388
<ul>
1389
{@faq()}
1390
</ul>
1391
</div>
1392
1393
# ~~~ FAQ END
1394
1395
1396
Subscription = rclass
1397
displayName : 'Subscription'
1398
1399
propTypes :
1400
redux : rtypes.object.isRequired
1401
subscription : rtypes.object.isRequired
1402
1403
getInitialState: ->
1404
confirm_cancel : false
1405
1406
cancel_subscription: ->
1407
@props.redux.getActions('billing').cancel_subscription(@props.subscription.id)
1408
1409
quantity: ->
1410
q = @props.subscription.quantity
1411
if q > 1
1412
return "#{q} × "
1413
1414
render_cancel_at_end: ->
1415
if @props.subscription.cancel_at_period_end
1416
<span style={marginLeft:'15px'}>Will cancel at period end.</span>
1417
1418
render_info: ->
1419
sub = @props.subscription
1420
cancellable = not (sub.cancel_at_period_end or @state.cancelling or @state.confirm_cancel)
1421
<Row style={paddingBottom: '5px', paddingTop:'5px'}>
1422
<Col md=4>
1423
{@quantity()} {sub.plan.name} ({misc.stripe_amount(sub.plan.amount, sub.plan.currency)} for {plan_interval(sub.plan)})
1424
</Col>
1425
<Col md=2>
1426
{misc.capitalize(sub.status)}
1427
</Col>
1428
<Col md=4 style={color:'#666'}>
1429
{misc.stripe_date(sub.current_period_start)} {misc.stripe_date(sub.current_period_end)} (start: {misc.stripe_date(sub.start)})
1430
{@render_cancel_at_end()}
1431
</Col>
1432
<Col md=2>
1433
{<Button style={float:'right'} onClick={=>@setState(confirm_cancel:true)}>Cancel...</Button> if cancellable}
1434
</Col>
1435
</Row>
1436
1437
render_confirm: ->
1438
if not @state.confirm_cancel
1439
return
1440
<Alert bsStyle='warning'>
1441
<Row style={borderBottom:'1px solid #999', paddingBottom:'15px', paddingTop:'15px'}>
1442
<Col md=6>
1443
Are you sure you want to cancel this subscription? If you cancel your subscription, it will run to the end of the subscription period, but will not be renewed when the current (already paid for) period ends; any upgrades provided by this subscription will be disabled. If you need further clarification or need a refund, please email <HelpEmailLink/>.
1444
</Col>
1445
<Col md=6>
1446
<Button onClick={=>@setState(confirm_cancel:false)}>Make no change</Button>
1447
<div style={float:'right'}>
1448
<Button bsStyle='danger' onClick={=>@setState(confirm_cancel:false);@cancel_subscription()}>CANCEL: do not auto-renew my subscription</Button>
1449
</div>
1450
</Col>
1451
</Row>
1452
</Alert>
1453
1454
1455
render: ->
1456
<div style={borderBottom:'1px solid #999', paddingTop: '5px', paddingBottom: '5px'}>
1457
{@render_info()}
1458
{@render_confirm() if @state.confirm_cancel}
1459
</div>
1460
1461
Subscriptions = rclass
1462
displayName : 'Subscriptions'
1463
1464
propTypes :
1465
subscriptions : rtypes.object
1466
sources : rtypes.object.isRequired
1467
selected_plan : rtypes.string
1468
redux : rtypes.object.isRequired
1469
1470
getInitialState: ->
1471
state : 'view' # view -> add_new -> # FUTURE: ??
1472
1473
render_add_subscription_button: ->
1474
<Button
1475
bsStyle = 'primary'
1476
disabled = {@state.state isnt 'view' or @props.sources.total_count is 0}
1477
onClick = {=>@setState(state : 'add_new')}
1478
className = 'pull-right' >
1479
<Icon name='plus-circle' /> Add Subscription...
1480
</Button>
1481
1482
render_add_subscription: ->
1483
<AddSubscription
1484
on_close = {=>@setState(state : 'view'); set_selected_plan('')}
1485
selected_plan = {@props.selected_plan}
1486
actions = {@props.redux.getActions('billing')} />
1487
1488
render_header: ->
1489
<Row>
1490
<Col sm=6>
1491
<Icon name='list-alt' /> Subscriptions
1492
</Col>
1493
<Col sm=6>
1494
{@render_add_subscription_button()}
1495
</Col>
1496
</Row>
1497
1498
render_subscriptions: ->
1499
for sub in @props.subscriptions.data
1500
<Subscription key={sub.id} subscription={sub} redux={@props.redux} />
1501
1502
render: ->
1503
<Panel header={@render_header()}>
1504
{@render_add_subscription() if @state.state is 'add_new'}
1505
{@render_subscriptions()}
1506
</Panel>
1507
1508
Invoice = rclass
1509
displayName : "Invoice"
1510
1511
propTypes :
1512
invoice : rtypes.object.isRequired
1513
redux : rtypes.object.isRequired
1514
1515
getInitialState: ->
1516
hide_line_items : true
1517
1518
download_invoice: (e) ->
1519
e.preventDefault()
1520
invoice = @props.invoice
1521
username = @props.redux.getStore('account').get_username()
1522
misc_page = require('./misc_page') # do NOT require at top level, since code in billing.cjsx may be used on backend
1523
misc_page.download_file("#{window.app_base_url}/invoice/sagemathcloud-#{username}-receipt-#{new Date(invoice.date*1000).toISOString().slice(0,10)}-#{invoice.id}.pdf")
1524
1525
render_paid_status: ->
1526
if @props.invoice.paid
1527
return <span>PAID</span>
1528
else
1529
return <span style={color:'red'}>UNPAID</span>
1530
1531
render_description: ->
1532
if @props.invoice.description
1533
return <span>{@props.invoice.description}</span>
1534
1535
render_line_description: (line) ->
1536
v = []
1537
if line.quantity > 1
1538
v.push("#{line.quantity} × ")
1539
if line.description?
1540
v.push(line.description)
1541
if line.plan?
1542
v.push(line.plan.name)
1543
v.push(" (start: #{misc.stripe_date(line.period.start)})")
1544
return v
1545
1546
render_line_item: (line, n) ->
1547
<Row key={line.id} style={borderBottom:'1px solid #aaa'}>
1548
<Col sm=1>
1549
{n}.
1550
</Col>
1551
<Col sm=9>
1552
{@render_line_description(line)}
1553
</Col>
1554
<Col sm=2>
1555
{render_amount(line.amount, @props.invoice.currency)}
1556
</Col>
1557
</Row>
1558
1559
render_tax: ->
1560
<Row key='tax' style={borderBottom:'1px solid #aaa'}>
1561
<Col sm=1>
1562
</Col>
1563
<Col sm=9>
1564
WA State Sales Tax ({@props.invoice.tax_percent}%)
1565
</Col>
1566
<Col sm=2>
1567
{render_amount(@props.invoice.tax, @props.invoice.currency)}
1568
</Col>
1569
</Row>
1570
1571
render_line_items: ->
1572
if @props.invoice.lines
1573
if @state.hide_line_items
1574
<a href='' onClick={(e)=>e.preventDefault();@setState(hide_line_items:false)}>(details)</a>
1575
else
1576
v = []
1577
v.push <a key='hide' href='' onClick={(e)=>e.preventDefault();@setState(hide_line_items:true)}>(hide details)</a>
1578
n = 1
1579
for line in @props.invoice.lines.data
1580
v.push @render_line_item(line, n)
1581
n += 1
1582
if @props.invoice.tax
1583
v.push @render_tax()
1584
return v
1585
1586
render: ->
1587
<Row style={borderBottom:'1px solid #999'}>
1588
<Col md=1>
1589
{render_amount(@props.invoice.amount_due, @props.invoice.currency)}
1590
</Col>
1591
<Col md=1>
1592
{@render_paid_status()}
1593
</Col>
1594
<Col md=3>
1595
{misc.stripe_date(@props.invoice.date)}
1596
</Col>
1597
<Col md=6>
1598
{@render_description()}
1599
{@render_line_items()}
1600
</Col>
1601
<Col md=1>
1602
<a onClick={@download_invoice} href=""><Icon name="cloud-download" /></a>
1603
</Col>
1604
</Row>
1605
1606
InvoiceHistory = rclass
1607
displayName : "InvoiceHistory"
1608
1609
propTypes :
1610
redux : rtypes.object.isRequired
1611
invoices : rtypes.object
1612
1613
render_header: ->
1614
<span>
1615
<Icon name="list-alt" /> Invoices and Receipts
1616
</span>
1617
1618
render_invoices: ->
1619
if not @props.invoices?
1620
return
1621
for invoice in @props.invoices.data
1622
<Invoice key={invoice.id} invoice={invoice} redux={@props.redux} />
1623
1624
render: ->
1625
<Panel header={@render_header()}>
1626
{@render_invoices()}
1627
</Panel>
1628
1629
exports.PayCourseFee = PayCourseFee = rclass
1630
propTypes :
1631
project_id : rtypes.string.isRequired
1632
redux : rtypes.object.isRequired
1633
1634
getInitialState: ->
1635
confirm : false
1636
1637
key: ->
1638
return "course-pay-#{@props.project_id}"
1639
1640
buy_subscription: ->
1641
actions = @props.redux.getActions('billing')
1642
# Set semething in billing store that says currently doing
1643
actions.setState("#{@key()}": true)
1644
# Purchase 1 course subscription
1645
actions.create_subscription('student_course')
1646
# Wait until a members-only upgrade and network upgrade are available, due to buying it
1647
@setState(confirm:false)
1648
@props.redux.getStore('account').wait
1649
until : (store) =>
1650
upgrades = store.get_total_upgrades()
1651
# NOTE! If you make one available due to changing what is allocated it won't cause this function
1652
# we're in here to update, since we *ONLY* listen to changes on the account store.
1653
applied = @props.redux.getStore('projects').get_total_upgrades_you_have_applied()
1654
return (upgrades.member_host ? 0) - (applied?.member_host ? 0) > 0 and (upgrades.network ? 0) - (applied?.network ? 0) > 0
1655
timeout : 30 # wait up to 30 seconds
1656
cb : (err) =>
1657
if err
1658
actions.setState(error:"Error purchasing course subscription: #{err}")
1659
else
1660
# Upgrades now available -- apply a network and members only upgrades to the course project.
1661
upgrades = {member_host: 1, network: 1}
1662
@props.redux.getActions('projects').apply_upgrades_to_project(@props.project_id, upgrades)
1663
# Set in billing that done
1664
actions.setState("#{@key()}": undefined)
1665
1666
render_buy_button: ->
1667
if @props.redux.getStore('billing').get(@key())
1668
<Button bsStyle='primary' disabled={true}>
1669
<Icon name="cc-icon-cocalc-ring" spin /> Paying the one-time $9 fee for this course...
1670
</Button>
1671
else
1672
<Button onClick={=>@setState(confirm:true)} disabled={@state.confirm} bsStyle='primary'>
1673
Pay the one-time $9 fee for this course...
1674
</Button>
1675
1676
render_confirm_button: ->
1677
if @state.confirm
1678
if @props.redux.getStore('account').get_total_upgrades().network > 0
1679
network = " and full internet access enabled"
1680
<Well style={marginTop:'1em'}>
1681
You will be charged a one-time $9 fee to move your project to a
1682
members-only server and enable full internet access.
1683
<br/><br/>
1684
<ButtonToolbar>
1685
<Button onClick={@buy_subscription} bsStyle='primary'>
1686
Pay $9 fee
1687
</Button>
1688
<Button onClick={=>@setState(confirm:false)}>Cancel</Button>
1689
</ButtonToolbar>
1690
</Well>
1691
1692
render: ->
1693
<span>
1694
{@render_buy_button()}
1695
{@render_confirm_button()}
1696
</span>
1697
1698
MoveCourse = rclass
1699
propTypes :
1700
project_id : rtypes.string.isRequired
1701
redux : rtypes.object.isRequired
1702
1703
getInitialState: ->
1704
confirm : false
1705
1706
upgrade: ->
1707
available = @props.redux.getStore('account').get_total_upgrades()
1708
upgrades = {member_host: 1}
1709
if available.network > 0
1710
upgrades.network = 1
1711
@props.redux.getActions('projects').apply_upgrades_to_project(@props.project_id, upgrades)
1712
@setState(confirm:false)
1713
1714
render_move_button: ->
1715
<Button onClick={=>@setState(confirm:true)} bsStyle='primary' disabled={@state.confirm}>
1716
Move this project to a members only server...
1717
</Button>
1718
1719
render_confirm_button: ->
1720
if @state.confirm
1721
if @props.redux.getStore('account').get_total_upgrades().network > 0
1722
network = " and full internet access enabled"
1723
<Well style={marginTop:'1em'}>
1724
Your project will be moved to a members only server{network} using
1725
upgrades included in your current subscription (no additional charge).
1726
<br/><br/>
1727
<ButtonToolbar>
1728
<Button onClick={@upgrade} bsStyle='primary'>
1729
Move Project
1730
</Button>
1731
<Button onClick={=>@setState(confirm:false)}>Cancel</Button>
1732
</ButtonToolbar>
1733
</Well>
1734
1735
render: ->
1736
<span>
1737
{@render_move_button()}
1738
{@render_confirm_button()}
1739
</span>
1740
1741
1742
BillingPage = rclass
1743
displayName : 'BillingPage'
1744
1745
reduxProps :
1746
billing :
1747
customer : rtypes.object
1748
invoices : rtypes.object
1749
error : rtypes.string
1750
action : rtypes.string
1751
loaded : rtypes.bool
1752
no_stripe : rtypes.bool # if true, stripe definitely isn't configured on the server
1753
selected_plan : rtypes.string
1754
projects :
1755
project_map : rtypes.immutable # used, e.g., for course project payments; also computing available upgrades
1756
account :
1757
stripe_customer : rtypes.immutable # to get total upgrades user has available
1758
1759
propTypes :
1760
redux : rtypes.object
1761
is_simplified : rtypes.bool
1762
for_course : rtypes.bool
1763
1764
render_action: ->
1765
if @props.action
1766
<div style={position:'relative', top:'-70px'}> {# probably ActivityDisplay should manage its own position better. }
1767
<ActivityDisplay activity ={[@props.action]} on_clear={=>@props.redux.getActions('billing').clear_action()} />
1768
</div>
1769
1770
render_error: ->
1771
if @props.error
1772
<ErrorDisplay
1773
error = {@props.error}
1774
onClose = {=>@props.redux.getActions('billing').clear_error()} />
1775
1776
render_help_suggestion: ->
1777
<span>
1778
<Space/> If you have any questions at all, email <HelpEmailLink /> immediately.
1779
<i>
1780
<Space/> Contact us if you are purchasing a course subscription, but need a short trial
1781
to test things out first.<Space/>
1782
</i>
1783
</span>
1784
1785
render_suggested_next_step: ->
1786
cards = @props.customer?.sources?.total_count ? 0
1787
subs = @props.customer?.subscriptions?.total_count ? 0
1788
invoices = @props.invoices?.data?.length ? 0
1789
help = @render_help_suggestion()
1790
1791
if cards == 0
1792
if subs == 0
1793
# no payment sources yet; no subscriptions either: a new user (probably)
1794
<span>
1795
Click "Add Payment Method..." to add your credit card, then
1796
click "Add Subscription..." and
1797
choose from either a monthly, yearly or semester-long plan.
1798
You will <b>not be charged</b> until you select a specific subscription then click
1799
"Add Subscription".
1800
{help}
1801
</span>
1802
else
1803
# subscriptions but they deleted their card.
1804
<span>
1805
Click "Add Payment Method..." to add a credit card so you can
1806
purchase or renew your subscriptions. Without a credit card
1807
any current subscriptions will run to completion, but will not renew.
1808
If you have any questions about subscriptions or billing (e.g., about
1809
using PayPal or wire transfers for non-recurring subscriptions above $50,
1810
please email <HelpEmailLink /> immediately.
1811
</span>
1812
1813
else if subs == 0
1814
# have a payment source, but no subscriptions
1815
<span>
1816
Click "Add Subscription...", then
1817
choose from either a monthly, yearly or semester-long plan (you may sign up for the
1818
same subscription more than once to increase the number of upgrades).
1819
You will be charged only after you select a specific subscription and click
1820
"Add Subscription".
1821
{help}
1822
</span>
1823
else if invoices == 0
1824
# have payment source, subscription, but no invoices yet
1825
<span>
1826
Sign up for the same subscription package more than
1827
once to increase the number of upgrades that you can use.
1828
{help}
1829
</span>
1830
else
1831
# have payment source, subscription, and at least one invoice
1832
<span>
1833
You may sign up for the same subscription package more than
1834
once to increase the number of upgrades that you can use.
1835
Past invoices and receipts are also available below.
1836
{help}
1837
</span>
1838
1839
render_info_link: ->
1840
<div style={marginTop:'1em', marginBottom:'1em', color:"#666"}>
1841
We offer many <a href=PolicyPricingPageUrl target='_blank'> pricing and subscription options</a>.
1842
<Space/>
1843
{@render_suggested_next_step()}
1844
</div>
1845
1846
get_panel_header: (icon, header) ->
1847
<div style={cursor:'pointer'} >
1848
<Icon name={icon} fixedWidth /> {header}
1849
</div>
1850
1851
render_subscriptions: ->
1852
<Subscriptions
1853
subscriptions = {@props.customer.subscriptions}
1854
sources = {@props.customer.sources}
1855
selected_plan = {@props.selected_plan}
1856
redux = {@props.redux} />
1857
1858
render_page: ->
1859
cards = @props.customer?.sources?.total_count ? 0
1860
subs = @props.customer?.subscriptions?.total_count ? 0
1861
if not @props.loaded
1862
# nothing loaded yet from backend
1863
<Loading />
1864
else if not @props.customer?
1865
# user not initialized yet -- only thing to do is add a card.
1866
<div>
1867
<PaymentMethods redux={@props.redux} sources={data:[]} default='' />
1868
</div>
1869
else
1870
# data loaded and customer exists
1871
if @props.is_simplified and subs > 0
1872
<div>
1873
<PaymentMethods redux={@props.redux} sources={@props.customer.sources} default={@props.customer.default_source} />
1874
{<Panel header={@get_panel_header('list-alt', 'Subscriptions')} eventKey='2'>
1875
{@render_subscriptions()}
1876
</Panel> if not @props.for_course}
1877
</div>
1878
else if @props.is_simplified
1879
<div>
1880
<PaymentMethods redux={@props.redux} sources={@props.customer.sources} default={@props.customer.default_source} />
1881
{@render_subscriptions() if not @props.for_course}
1882
</div>
1883
else
1884
<div>
1885
<PaymentMethods redux={@props.redux} sources={@props.customer.sources} default={@props.customer.default_source} />
1886
{@render_subscriptions() if not @props.for_course}
1887
<InvoiceHistory invoices={@props.invoices} redux={@props.redux} />
1888
</div>
1889
1890
render: ->
1891
<div>
1892
<div>
1893
{@render_info_link() if not @props.for_course}
1894
{@render_action() if not @props.no_stripe}
1895
{@render_error()}
1896
{@render_page() if not @props.no_stripe}
1897
</div>
1898
{<Footer/> if not @props.is_simplified}
1899
</div>
1900
1901
exports.BillingPageRedux = rclass
1902
displayName : 'BillingPage-redux'
1903
1904
render: ->
1905
<BillingPage is_simplified={false} redux={redux} />
1906
1907
exports.BillingPageSimplifiedRedux = rclass
1908
displayName : 'BillingPage-redux'
1909
1910
render: ->
1911
<BillingPage is_simplified={true} redux={redux} />
1912
1913
exports.BillingPageForCourseRedux = rclass
1914
displayName : 'BillingPage-redux'
1915
1916
render: ->
1917
<BillingPage is_simplified={true} for_course={true} redux={redux} />
1918
1919
render_amount = (amount, currency) ->
1920
<div style={float:'right'}>{misc.stripe_amount(amount, currency)}</div>
1921
1922
brand_to_icon = (brand) ->
1923
if brand in ['discover', 'mastercard', 'visa'] then "cc-#{brand}" else "credit-card"
1924
1925
COUNTRIES = ",United States,Canada,Spain,France,United Kingdom,Germany,Russia,Colombia,Mexico,Italy,Afghanistan,Albania,Algeria,American Samoa,Andorra,Angola,Anguilla,Antarctica,Antigua and Barbuda,Argentina,Armenia,Aruba,Australia,Austria,Azerbaijan,Bahamas,Bahrain,Bangladesh,Barbados,Belarus,Belgium,Belize,Benin,Bermuda,Bhutan,Bolivia,Bosnia and Herzegovina,Botswana,Bouvet Island,Brazil,British Indian Ocean Territory,British Virgin Islands,Brunei,Bulgaria,Burkina Faso,Burundi,Cambodia,Cameroon,Canada,Cape Verde,Cayman Islands,Central African Republic,Chad,Chile,China,Christmas Island,Cocos (Keeling) Islands,Colombia,Comoros,Congo,Cook Islands,Costa Rica,Cote d'Ivoire,Croatia,Cuba,Cyprus,Czech Republic,Democratic Republic of The Congo,Denmark,Djibouti,Dominica,Dominican Republic,Ecuador,Egypt,El Salvador,Equatorial Guinea,Eritrea,Estonia,Ethiopia,Falkland Islands,Faroe Islands,Fiji,Finland,France,French Guiana,French Polynesia,French Southern and Antarctic Lands,Gabon,Gambia,Georgia,Germany,Ghana,Gibraltar,Greece,Greenland,Grenada,Guadeloupe,Guam,Guatemala,Guinea,Guinea-Bissau,Guyana,Haiti,Heard Island and McDonald Islands,Honduras,Hong Kong,Hungary,Iceland,India,Indonesia,Iran,Iraq,Ireland,Israel,Italy,Jamaica,Japan,Jordan,Kazakhstan,Kenya,Kiribati,Kuwait,Kyrgyzstan,Laos,Latvia,Lebanon,Lesotho,Liberia,Libya,Liechtenstein,Lithuania,Luxembourg,Macao,Macedonia,Madagascar,Malawi,Malaysia,Maldives,Mali,Malta,Marshall Islands,Martinique,Mauritania,Mauritius,Mayotte,Mexico,Micronesia,Moldova,Monaco,Mongolia,Montenegro,Montserrat,Morocco,Mozambique,Myanmar,Namibia,Nauru,Nepal,Netherlands,Netherlands Antilles,New Caledonia,New Zealand,Nicaragua,Niger,Nigeria,Niue,Norfolk Island,North Korea,Northern Mariana Islands,Norway,Oman,Pakistan,Palau,Palestine,Panama,Papua New Guinea,Paraguay,Peru,Philippines,Pitcairn Islands,Poland,Portugal,Puerto Rico,Qatar,Reunion,Romania,Rwanda,Saint Helena,Saint Kitts and Nevis,Saint Lucia,Saint Pierre and Miquelon,Saint Vincent and The Grenadines,Samoa,San Marino,Sao Tome and Principe,Saudi Arabia,Senegal,Serbia,Seychelles,Sierra Leone,Singapore,Slovakia,Slovenia,Solomon Islands,Somalia,South Africa,South Georgia and The South Sandwich Islands,South Korea,South Sudan,Spain,Sri Lanka,Sudan,Suriname,Svalbard and Jan Mayen,Swaziland,Sweden,Switzerland,Syria,Taiwan,Tajikistan,Tanzania,Thailand,Timor-Leste,Togo,Tokelau,Tonga,Trinidad and Tobago,Tunisia,Turkey,Turkmenistan,Turks and Caicos Islands,Tuvalu,Uganda,Ukraine,United Arab Emirates,United Kingdom,United States,United States Minor Outlying Islands,Uruguay,Uzbekistan,Vanuatu,Vatican City,Venezuela,Vietnam,Wallis and Futuna,Western Sahara,Yemen,Zambia,Zimbabwe".split(',')
1926
1927
STATES = {'':'',AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming',AS:'American Samoa',DC:'District of Columbia',GU:'Guam',MP:'Northern Mariana Islands',PR:'Puerto Rico',VI:'United States Virgin Islands'}
1928
1929
1930
# FUTURE: make this an action and a getter in the BILLING store
1931
set_selected_plan = (plan, period) ->
1932
if period?.slice(0,4) == 'year'
1933
plan = plan + "-year"
1934
redux.getActions('billing').setState(selected_plan : plan)
1935
1936
exports.render_static_pricing_page = () ->
1937
<div>
1938
<ExplainResources type='shared' is_static={true}/>
1939
<hr/>
1940
<ExplainPlan type='personal'/>
1941
<SubscriptionGrid period='month year' is_static={true}/>
1942
{# <Space/><ExplainResources type='dedicated'/> }
1943
<hr/>
1944
<ExplainPlan type='course'/>
1945
<SubscriptionGrid period='month4 year1' is_static={true}/>
1946
<hr/>
1947
<FAQ/>
1948
</div>
1949
1950
exports.visit_billing_page = ->
1951
require('./history').load_target('settings/billing')
1952
1953
exports.BillingPageLink = (opts) ->
1954
{text} = opts
1955
if not text
1956
text = "billing page"
1957
return <a onClick={exports.visit_billing_page} style={cursor:'pointer'}>{text}</a>
1958
1959
plan_interval = (plan) ->
1960
n = plan.interval_count
1961
return "#{plan.interval_count} #{misc.plural(n, plan.interval)}"
1962