482 lines
19 KiB
CoffeeScript
482 lines
19 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
|
|
}
|
|
|
|
beforeHide: (e) ->
|
|
@resetErrors()
|
|
|
|
beforeShow: (e) ->
|
|
|
|
afterShow: (e) ->
|
|
@resetState()
|
|
@resetErrors()
|
|
@setState({updating: true})
|
|
rest.getUnprocessedLessonOrIntent().done((response) => @unprocessLoaded(response)).fail((jqXHR) => @failedBooking(jqXHR))
|
|
|
|
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})
|
|
|
|
unprocessLoaded: (response) ->
|
|
@setState({updating: false})
|
|
logger.debug("unprocessed loaded", response)
|
|
@setState(response)
|
|
|
|
failedBooking: (jqXHR) ->
|
|
@setState({updating: false})
|
|
@setState({lesson: null})
|
|
if jqXHR.status == 404
|
|
# no unprocessed lessons. That's arguably OK; the user is just going to enter their info up front.
|
|
console.log("nothing")
|
|
|
|
failedUnprocessLoad: (jqXHR) ->
|
|
@setState({updating: false})
|
|
@app.layout.notify({
|
|
title: 'Unable to load lesson',
|
|
text: 'Please attempt to book a free lesson first or refresh this page.'
|
|
})
|
|
|
|
onBack: (e) ->
|
|
e.preventDefault()
|
|
window.location.href = '/client#/teachers/search'
|
|
|
|
onSubmit: (e) ->
|
|
@resetErrors()
|
|
|
|
e.preventDefault()
|
|
|
|
if !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 !@state.userWantsUpdateCC
|
|
@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,
|
|
}
|
|
|
|
window.Stripe.card.createToken(data, (status, response) => (@stripeResponseHandler(status, response)));
|
|
|
|
stripeResponseHandler: (status, response) ->
|
|
console.log("response", response)
|
|
|
|
if response.error
|
|
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)
|
|
|
|
attemptPurchase: (token) ->
|
|
if this.state.billingInUS
|
|
zip = @root.find('input.zip').val()
|
|
|
|
data = {
|
|
token: token,
|
|
zip: zip,
|
|
test_drive: @state.lesson?.lesson_type == 'test-drive' || (@state.intent?.intent == 'book-test-drive')
|
|
}
|
|
|
|
if @state.shouldShowName
|
|
data.name = @root.find('#set-user-on-card').val()
|
|
|
|
rest.submitStripe(data).done((response) => @stripeSubmitted(response)).fail((jqXHR) => @stripeSubmitFailure(jqXHR))
|
|
|
|
stripeSubmitted: (response) ->
|
|
logger.debug("stripe submitted", response)
|
|
|
|
if @state.shouldShowName
|
|
window.UserActions.refresh()
|
|
|
|
# if the response has a lesson, take them there
|
|
if response.lesson?.id?
|
|
context.Banner.showNotice({
|
|
title: "Lesson Requested",
|
|
text: "The teacher has been notified of your lesson request, and should respond soon.<br/><br/>We've taken you automatically to the page for this request, and sent an email to you with a link here as well. All communication with the teacher will show up on this page and in email."
|
|
})
|
|
window.location = "/client#/jamclass/lesson-session/" + response.lesson.id
|
|
else if response.test_drive? || response.intent_book_test_drive?
|
|
|
|
if response.test_drive?.teacher_id
|
|
teacher_id = response.test_drive?.teacher_id
|
|
else if response.intent_book_test_drive?
|
|
teacher_id = response.intent_book_test_drive?.teacher_id
|
|
|
|
if teacher_id?
|
|
text = "You now have 4 lessons that you can take with 4 different teachers.<br/><br/>We've taken you automatically to the lesson booking screen for the teacher you initially showed interest in."
|
|
location = "/client#/profile/teacher/" + teacher_id
|
|
location = "/client#/jamclass/book-lesson/test-drive_" + teacher_id
|
|
else
|
|
text = "You now have 4 lessons that you can take with 4 different teachers.<br/><br/>We've taken you automatically to the Teacher Search screen, so you can search for teachers right for you."
|
|
location = "/client#/teachers/search"
|
|
context.JK.Banner.showNotice({
|
|
title: "Test Drive Purchased",
|
|
text: text
|
|
})
|
|
window.location = location
|
|
else
|
|
window.location = "/client#/teachers/search"
|
|
|
|
stripeSubmitFailure: (jqXHR) ->
|
|
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?']
|
|
|
|
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.intent?
|
|
if @state.lesson?
|
|
photo_url = @state.lesson.teacher.photo_url
|
|
name = @state.lesson.teacher.name
|
|
lesson_length = this.state.lesson.lesson_length
|
|
lesson_type = this.state.lesson.lesson_type
|
|
else
|
|
photo_url = @state.intent.teacher.photo_url
|
|
name = @state.intent.teacher.name
|
|
lesson_length = 30
|
|
lesson_type = @state.intent.intent.substring('book-'.length)
|
|
|
|
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 lesson_type == 'single-free'
|
|
header = `<div><h2>enter card info</h2>
|
|
|
|
<div className="no-charge">Your card wil not be charged.<br/>See explanation to the right.</div>
|
|
</div>`
|
|
bookingInfo = `<p>You are booking a single free {lesson_length}-minute lesson.</p>`
|
|
bookingDetail = `<p>To book this lesson, you will need to enter your credit card information.
|
|
You will absolutely not be charged for this free lesson, and you have no further commitment to purchase
|
|
anything. We have to collect a credit card to prevent abuse by some users who would otherwise set up
|
|
multiple free accounts to get multiple free lessons.
|
|
<br/>
|
|
|
|
<div className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
|
policies</a></div>
|
|
</p>`
|
|
else if lesson_type == 'test-drive'
|
|
|
|
|
|
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>`
|
|
bookingDetail = `<p>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.
|
|
<br/>
|
|
|
|
<span className="jamclass-policies"><a href="/corp/terms" rel="external" onClick={this.jamclassPolicies}>jamclass
|
|
policies</a></span>
|
|
</p>`
|
|
else if lesson_type == 'paid'
|
|
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.state.lesson.booked_price.toFixed(2)}</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.state.lesson.booked_price.toFixed(2)}</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
|
|
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, 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>`
|
|
|
|
}) |