jam-cloud/web/app/assets/javascripts/react-components/LessonPayment.js.jsx.coffee

594 lines
23 KiB
CoffeeScript

context = window
rest = context.JK.Rest()
logger = context.JK.logger
UserStore = context.UserStore
@LessonPayment = React.createClass({
mixins: [
ICheckMixin,
Reflux.listenTo(AppStore, "onAppInit"),
Reflux.listenTo(UserStore, "onUserChanged")
]
shouldShowNameSet: false
onAppInit: (@app) ->
@app.bindScreen('jamclass/lesson-payment',
{beforeShow: @beforeShow, afterShow: @afterShow, beforeHide: @beforeHide})
onUserChanged: (userState) ->
if !@shouldShowNameSet
@shouldShowNameSet = true
if userState?.user?
username = userState.user.name
first_name = userState.user.first_name
last_name = userState.user.last_name
shouldShowName = !username? || username.trim() == '' || username.toLowerCase().indexOf('anonymous') > -1
else
shouldShowName = @state.shouldShowName
@setState({user: userState?.user, shouldShowName: shouldShowName})
componentDidMount: () ->
@checkboxes = [{selector: 'input.billing-address-in-us', stateKey: 'billingInUS'}]
@root = $(@getDOMNode())
@root.find('input.expiration').payment('formatCardExpiry')
@root.find("input.card-number").payment('formatCardNumber')
@root.find("input.cvv").payment('formatCardCVC')
@iCheckify()
componentDidUpdate: () ->
@iCheckify()
getInitialState: () ->
{
user: null,
lesson: null,
updating: false,
billingInUS: true,
userWantsUpdateCC: false,
"test-drive": false,
teacher: null
}
beforeHide: (e) ->
@resetErrors()
beforeShow: (e) ->
afterShow: (e) ->
@resetState()
@resetErrors()
parsed = @parseId(e.id)
parsed.updating = false
if parsed['lesson-booking']
parsed.updating = true
rest.getLessonBooking({id: parsed.lesson_booking_id}).done((response) => @lessonBookingLoaded(response)).fail((jqXHR) => @failedLessonBooking(jqXHR))
else if parsed['teacher-intent']
parsed.updating = true
rest.getUserDetail({id: parsed.teacher_id}).done((response) => @teacherLoaded(response)).fail((jqXHR) => @failedTeacher(jqXHR))
else if parsed['test-drive']
logger.debug("test-drive lesson payment; no teacher/booking in context")
else
logger.error("unknown state for lesson-payment")
window.location.href = '/client#/jamclass'
@setState(parsed)
parseId: (id) ->
result = {}
# id can be:
# 'test-drive'
# or 'lesson-booking_id'
# or 'teacher_id
result['test-drive'] = false
result['lesson-booking'] = false
result['teacher-intent'] = false
bits = id.split('_')
if bits.length == 1
# should be id=test-drive
result[id] = true
else if bits.length == 2
type = bits[0]
if type == 'lesson-booking'
result[type] = true
result.lesson_booking_id = bits[1]
else if type == 'teacher'
result['teacher-intent'] = true
result.teacher_id = bits[1]
logger.debug("LessonPayment: parseId " + JSON.stringify(result))
result
resetErrors: () ->
@setState({ccError: null, cvvError: null, expiryError: null, billingInUSError: null, zipCodeError: null, nameError: null})
checkboxChanged: (e) ->
checked = $(e.target).is(':checked')
@setState({billingInUS: checked})
resetState: () ->
@setState({updating: false, lesson: null, teacher: null, "test-drive": false, "lesson-booking" : false, "teacher-intent": false})
lessonBookingLoaded: (response) ->
@setState({updating: false})
logger.debug("lesson booking loaded", response)
if response.card_presumed_ok
context.JK.Banner.showNotice("Lesson Already Requested", "You have already requested this lesson from this teacher.")
window.location.href = "/client#/jamclass"
@setState({lesson: response, teacher: response.teacher})
failedLessonBooking: (jqXHR) ->
@setState({updating: false})
@app.layout.notify({
title: 'unable to load lesson info',
text: 'Something has gone wrong. Please try refreshing the page.'
})
teacherLoaded: (response) ->
@setState({updating: false})
logger.debug("teacher loaded", response)
@setState({teacher: response})
failedTeacher: (jqXHR) ->
@setState({updating: false})
@app.layout.notify({
title: 'unable to load teacher info',
text: 'Something has gone wrong. Please try refreshing the page.'
})
onBack: (e) ->
e.preventDefault()
window.location.href = '/client#/teachers/search'
onSubmit: (e) ->
@resetErrors()
e.preventDefault()
if !window.Stripe?
logger.error("no window.Stripe")
@app.layout.notify({
title: 'Payment System Not Loaded',
text: "Please refresh this page and try to enter your info again. Sorry for the inconvenience!"
})
else
if @reuseStoredCard()
@attemptPurchase(null)
else
ccNumber = @root.find('input.card-number').val()
expiration = @root.find('input.expiration').val()
cvv = @root.find('input.cvv').val()
inUS = @root.find('input.billing-address-in-us').is(':checked')
zip = @root.find('input.zip').val()
error = false
if @state.shouldShowName
name = @root.find('#set-user-on-card').val()
if name.indexOf('Anonymous') > -1
@setState({nameError: true})
error = true
if !$.payment.validateCardNumber(ccNumber)
@setState({ccError: true})
error = true
bits = expiration.split('/')
if bits.length == 2
month = bits[0].trim();
year = bits[1].trim()
month = new Number(month)
year = new Number(year)
if year < 2000
year += 2000
if !$.payment.validateCardExpiry(month, year)
@setState({expiryError: true})
error = true
else
@setState({expiryError: true})
error = true
cardType = $.payment.cardType(ccNumber)
if !$.payment.validateCardCVC(cvv, cardType)
@setState({cvvError: true})
error = true
if inUS && (!zip? || zip == '')
@setState({zipCodeError: true})
if error
return
data = {
number: ccNumber,
cvc: cvv,
exp_month: month,
exp_year: year,
}
@setState({updating: true})
logger.debug("creating stripe token")
window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response)));
stripeResponseHandler: (status, response) ->
console.log("stripe response", JSON.stringify(response))
if response.error
@setState({updating: false})
if response.error.code == "invalid_number"
@setState({ccError: true, cvvError: null, expiryError: null})
else if response.error.code == "invalid_cvc"
@setState({ccError: null, cvvError: true, expiryError: null})
else if response.error.code == "invalid_expiry_year" || response.error.code == "invalid_expiry_month"
@setState({ccError: null, cvvError: null, expiryError: true})
else
@attemptPurchase(response.id)
isNormal: () ->
@state.lesson?.lesson_type == 'paid'
isTestDrive: () ->
@state['test-drive'] == true || @state.lesson?.lesson_type == 'test-drive' || @state['teacher-intent']
attemptPurchase: (token) ->
if this.state.billingInUS
zip = @root.find('input.zip').val()
data = {
token: token,
zip: zip,
test_drive: @isTestDrive(),
booking_id: @state.lesson?.id,
normal: @isNormal()
}
if @state.shouldShowName
data.name = @root.find('#set-user-on-card').val()
@setState({updating: true})
logger.debug("submitting purchase info: " + JSON.stringify(data))
rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
stripeSubmitted: (response) ->
@setState({updating: false})
logger.debug("stripe submitted: " + JSON.stringify(response))
#if @state.shouldShowName
window.UserActions.refresh()
# if the response has a lesson, take them there
if response.test_drive?
# ok, they bought a package
if response.lesson_package_type?
# always of form test-drive-#
prefixLength = "test-drive-".length
packageLength = response.lesson_package_type.package_type.length
testDriveCount = response.lesson_package_type.package_type.substring(prefixLength, packageLength)
logger.debug("testDriveCount: " + testDriveCount)
if response.test_drive?.teacher_id
teacher_id = response.test_drive.teacher_id
if testDriveCount == '1'
text = "You have purchased 1 TestDrive credit and have used it to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
else
text = "You have purchased #{testDriveCount} TestDrive credits and have used 1 credit to request a JamClass with #{@state.teacher.name}. The teacher has received your request and should respond shortly."
location = "/client#/jamclass"
else
if @state.teacher?.id
# the user bought the testdrive, and there is a teacher of interest in context (but no booking)
if testDriveCount == '1'
text = "You now have 1 TestDrive credit.<br/><br/>We've taken you to the lesson booking screen for the teacher you initially showed interest in."
location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
else
text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.<br/><br/>We've taken you to the lesson booking screen for the teacher you initially showed interest in."
location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
else
# the user bought test drive, but 'cold' , i.e., no teacher in context
if testDriveCount == '1'
text = "You now have 1 TestDrive credit.<br/><br/>We've taken you to the Teacher Search screen, so you can search for teachers right for you."
location = "/client#/teachers/search"
else
text = "You now have #{testDriveCount} TestDrive credits that you can take with #{testDriveCount} different teachers.<br/><br/>We've taken you to the Teacher Search screen, so you can search for teachers right for you."
location = "/client#/teachers/search"
context.JK.Banner.showNotice("Test Drive Purchased",text)
window.location = location
else
context.JK.Banner.showNotice("Something Went Wrong", "Please email support@jamkazam.com and indicate that your attempt to buy a TestDrive failed")
window.location = "/client#/jamclass/"
else if response.lesson?.id?
context.JK.Banner.showNotice("Lesson Requested","The teacher has been notified of your lesson request, and should respond soon.<br/><br/>We've taken you back to the JamClass home page, where you can check the status of this lesson, as well as any other past and future lessons.")
url = "/client#/jamclass/lesson-booking/" + response.lesson.id
url = "/client#/jamclass"
window.location.href = url
else
window.location = "/client#/teachers/search"
stripeSubmitFailure: (jqXHR) ->
logger.debug("stripe submission failure", jqXHR.responseText)
@setState({updating: false})
handled = false
if jqXHR.status == 422
errors = JSON.parse(jqXHR.responseText)
if errors.errors.name?
@setState({name: errors.errors.name[0]})
handled = true
else if errors.errors.user?
@app.layout.notify({title: "Can't Purchase Test Drive", text: "You " + errors.errors.user[0] + '.' })
handled = true
if !handled
@app.notifyServerError(jqXHR, 'Credit Card Not Stored')
onUnlockPaymentInfo: (e) ->
e.preventDefault()
@setState({userWantsUpdateCC: true})
onLockPaymentInfo: (e) ->
e.preventDefault()
@setState({userWantsUpdateCC: false})
reuseStoredCard: () ->
!@state.userWantsUpdateCC && @state.user?['has_stored_credit_card?']
bookedPrice: () ->
booked_price = this.state.lesson.booked_price
if booked_price?
if typeof booked_price == "string"
booked_price = new Number(booked_price)
return booked_price.toFixed(2)
else
return '??'
render: () ->
disabled = @state.updating || @reuseStoredCard()
if @state.updating
photo_url = '/assets/shared/avatar_generic.png'
name = 'Loading ...'
teacherDetails = `<div className="teacher-header">
<div className="avatar">
<img src={photo_url}/>
</div>
{name}
</div>`
else
if @state.lesson? || @state['test-drive'] || @state.teacher?
if @state.teacher?
photo_url = @state.teacher.photo_url
name = @state.teacher.name
if !photo_url?
photo_url = '/assets/shared/avatar_generic.png'
teacherDetails = `<div className="teacher-header">
<div className="avatar">
<img src={photo_url}/>
</div>
{name}
</div>`
if @state.lesson?
lesson_length = @state.lesson.lesson_length
lesson_type = @state.lesson.lesson_type
else
lesson_length = 30
lesson_type = 'test-drive'
if @isTestDrive()
if @reuseStoredCard()
header = `<div><h2>purchase test drive</h2></div>`
else
header = `<div><h2>enter payment info for test drive</h2></div>`
bookingInfo = `<p></p>`
if this.state.user.lesson_package_type_id == 'test-drive'
explanation = `<span>You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 4 private online music lessons - 1 each from 4 different instructors in the JamClass instructor community. The price of this TestDrive package is $49.99.</span>`
else if this.state.user.lesson_package_type_id == 'test-drive-1'
explanation =`<span>You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 1 private online music lesson from an instructor in the JamClass instructor community. The price of this TestDrive package is $14.99.</span>`
else if this.state.user.lesson_package_type_id == 'test-drive-2'
explanation =`<span>You are purchasing the TestDrive package of JamClass by JamKazam. This purchase entitles you to take 2 private online music lessons - 1 each from 2 different instructors in the JamClass instructor community. The price of this TestDrive package is $29.99.</span>`
else
alert("You do not have a test drive package selected")
bookingDetail = `<p>{explanation}
<br/>
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
policies</a></span>
</p>`
else if @isNormal()
if @reuseStoredCard()
header = `<div><h2>purchase lesson</h2></div>`
else
header = `<div><h2>enter payment info for lesson</h2></div>`
if this.state.lesson.recurring
if this.state.lesson.payment_style == 'single'
bookingInfo = `<p>You are booking a {lesson_length} minute lesson for
${this.bookedPrice()}</p>`
bookingDetail = `<p>
Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your
lesson is scheduled, or you will be charged for the lesson in full.
<br/>
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
policies</a></span>
</p>`
else if this.state.lesson.payment_style == 'weekly'
bookingInfo = `<p>You are booking a weekly recurring series of {lesson_length}-minute
lessons, to be paid individually as each lesson is taken, until cancelled.</p>`
bookingDetail = `<p>
Your card will be charged on the day of each lesson. If you need to cancel a lesson, you must do so at
least 24 hours before the lesson is scheduled, or you will be charged for the lesson in full.
<br/>
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
policies</a></span>
</p>`
else if this.state.lesson.payment_style == 'monthly'
bookingInfo = `<p>You are booking a weekly recurring series of {lesson_length}-minute
lessons, to be paid for monthly until cancelled.</p>`
bookingDetail = `<p>
Your card will be charged on the first day of each month. Canceling individual lessons does not earn a
refund when buying monthly. To cancel, you must cancel at least 24 hours before the beginning of the
month, or you will be charged for that month in full.
<br/>
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
policies</a></span>
</p>`
else
bookingInfo = `<p>You are booking a {lesson_length} minute lesson for
${this.bookedPrice()}</p>`
bookingDetail = `<p>
Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your
lesson is scheduled, or you will be charged for the lesson in full.
<br/>
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
policies</a></span>
</p>`
else
if @reuseStoredCard()
header = `<div><h2>payment info already entered</h2></div>`
else
header = `<div><h2>enter payment info</h2></div>`
bookingInfo = `<p>You are entering your credit card info so that later checkouts go quickly. You can skip this
for now.</p>`
bookingDetail = `
<p>
Your card will not be charged until the day of the lesson. You must cancel at least 24 hours before your
lesson is scheduled, or you will be charged for the lesson in full.
<br/>
<span className="jamclass-policies plain"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass policies</a></span>
</p>`
submitClassNames = {'button-orange': true, 'purchase-btn': true, disabled: disabled && @state.updating}
updateCardClassNames = {'button-grey': true, disabled: disabled && @state.updating}
backClassNames = {'button-grey': true, disabled: disabled && @state.updating}
cardNumberFieldClasses = {field: true, "card-number": true, error: @state.ccError}
expirationFieldClasses = {field: true, "expiration": true, error: @state.expiryError}
cvvFieldClasses = {field: true, "card-number": true, error: @state.cvvError}
inUSClasses = {field: true, "billing-in-us": true, error: @state.billingInUSError}
zipCodeClasses = {field: true, "zip-code": true, error: @state.zipCodeError}
nameClasses= {field: true, "name": true, error: @state.nameError}
formClasses= {stored: @reuseStoredCard()}
leftColumnClasses = {column: true, 'column-left': true, stored: @reuseStoredCard()}
rightColumnClasses = {column: true, 'column-right': true, stored: @reuseStoredCard()}
if @state.user?['has_stored_credit_card?']
if @state.userWantsUpdateCC
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onLockPaymentInfo}>NEVERMIND, USE MY STORED PAYMENT INFO</a>`
leftPurchaseActions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
<a className={classNames(submitClassNames)} onClick={this.onSubmit}>PURCHASE</a>
</div>`
else
updateCardAction = `<a className={classNames(updateCardClassNames)} onClick={this.onUnlockPaymentInfo}>I'D LIKE TO UPDATE MY PAYMENT INFO</a>`
rightPurchaseActions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a>
{updateCardAction}
<a className={classNames(submitClassNames)} onClick={this.onSubmit}>PURCHASE</a>
</div>`
else
leftPurchaseActions = `<div className="actions">
<a className={classNames(backClassNames)} onClick={this.onBack}>BACK</a><a
className={classNames(submitClassNames)} onClick={this.onSubmit}>SUBMIT CARD INFORMATION</a>
</div>`
if @state.shouldShowName && @state.user?.name?
username = @state.user?.name
nameField =
`<div className={classNames(nameClasses)}>
<label>Name:</label>
<input id="set-user-on-card" disabled={disabled} type="text" name="name" className="name" defaultValue={username}></input>
</div>`
`<div className="content-body-scroller">
<div className={classNames(leftColumnClasses)}>
{header}
<form autoComplete="on" onSubmit={this.onSubmit} className={classNames(formClasses)}>
{nameField}
<div className={classNames(cardNumberFieldClasses)}>
<label>Card Number:</label>
<input placeholder="1234 5678 9123 4567" type="tel" autoComplete="cc-number" disabled={disabled}
type="text" name="card-number" className="card-number"></input>
</div>
<div className={classNames(expirationFieldClasses)}>
<label>Expiration Date:</label>
<input placeholder="MM / YY" autoComplete="cc-expiry" disabled={disabled} type="text" name="expiration"
className="expiration"></input>
</div>
<div className={classNames(cvvFieldClasses)}>
<label>CVV:</label>
<input autoComplete="off" disabled={disabled} type="text" name="cvv" className="cvv"></input>
</div>
<div className={classNames(zipCodeClasses)}>
<label>Zip Code</label>
<input autoComplete="off" disabled={disabled || !this.state.billingInUS} type="text" name="zip"
className="zip"></input>
</div>
<div className={classNames(inUSClasses)}>
<label>Billing Address<br/>is in the U.S.</label>
<input type="checkbox" name="billing-address-in-us" className="billing-address-in-us"
value={this.state.billingInUS}/>
</div>
<input style={{'display':'none'}} type="submit" name="submit"/>
</form>
{leftPurchaseActions}
</div>
<div className={classNames(rightColumnClasses)}>
{teacherDetails}
<div className="booking-info">
{bookingInfo}
{bookingDetail}
{rightPurchaseActions}
</div>
</div>
<br className="clearall"/>
</div>`
})