paypal support

This commit is contained in:
Seth Call 2016-12-15 12:47:08 -06:00
parent 458637b1de
commit 5e04f72d8e
36 changed files with 1668 additions and 703 deletions

View File

@ -369,4 +369,5 @@ second_ed.sql
second_ed_v2.sql second_ed_v2.sql
retailers_v2.sql retailers_v2.sql
retailer_interest.sql retailer_interest.sql
connection_role.sql connection_role.sql
retailer_payment_split.sql

View File

@ -0,0 +1,2 @@
ALTER TABLE retailers ADD COLUMN payment VARCHAR;
ALTER TABLE lesson_bookings ADD COLUMN payment VARCHAR;

View File

@ -40,6 +40,7 @@ require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error" require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/errors/jam_record_not_found" require "jam_ruby/errors/jam_record_not_found"
require "jam_ruby/errors/conflict_error" require "jam_ruby/errors/conflict_error"
require "jam_ruby/errors/pay_pal_client_error"
require "jam_ruby/lib/app_config" require "jam_ruby/lib/app_config"
require "jam_ruby/lib/s3_manager_mixin" require "jam_ruby/lib/s3_manager_mixin"
require "jam_ruby/lib/s3_public_manager_mixin" require "jam_ruby/lib/s3_public_manager_mixin"

View File

@ -0,0 +1,19 @@
module JamRuby
class PayPalClientError < StandardError
attr_accessor :errors
def initialize(data)
if data.respond_to?('has_key?')
self.errors = data
else
self.errors = {:message=>data.to_s}
end
end # initialize
def to_s
s=super
s << ", errors: #{errors.inspect}" if self.errors.any?
s
end
end
end

View File

@ -531,7 +531,7 @@ module JamRuby
distribution = teacher_distribution_price_in_cents(target) distribution = teacher_distribution_price_in_cents(target)
if education if education
(distribution * 0.0625).round (distribution * 0.0625).round # 0.0625 is 1/4th of 25%
else else
distribution distribution
end end

View File

@ -216,11 +216,20 @@ module JamRuby
self.status = STATUS_COMPLETED self.status = STATUS_COMPLETED
# RETAILERPAY2
if success && lesson_booking.requires_teacher_distribution?(self) if success && lesson_booking.requires_teacher_distribution?(self)
is_education_school_on_school = lesson_booking.school_on_school_payment?
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false) self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false)
if lesson_booking.school_on_school_payment? if is_education_school_on_school
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true) self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true)
end end
# this is a bit of a hack, in how the code is structured.
# but basically, the distributions calculated are too dynamic for the above code.
# if this is a retailer
end end
if self.save if self.save

View File

@ -9,6 +9,7 @@ module JamRuby
SOURCE_RECURLY = 'recurly' SOURCE_RECURLY = 'recurly'
SOURCE_IOS = 'ios' SOURCE_IOS = 'ios'
SOURCE_PAYPAL = 'paypal'
belongs_to :retailer, class_name: 'JamRuby::Retailer' belongs_to :retailer, class_name: 'JamRuby::Retailer'
belongs_to :user, class_name: 'JamRuby::User' belongs_to :user, class_name: 'JamRuby::User'
@ -166,7 +167,7 @@ module JamRuby
# individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation) # individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
# jamtracks however can be piled onto the same sale as adjustments (VRFS-3028) # jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
# so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack # so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
def self.place_order(current_user, shopping_carts) def self.place_order(current_user, shopping_carts, paypal = false)
sales = [] sales = []
@ -176,7 +177,7 @@ module JamRuby
# return sales # return sales
#end #end
jam_track_sale = order_jam_tracks(current_user, shopping_carts) jam_track_sale = order_jam_tracks(current_user, shopping_carts, paypal)
sales << jam_track_sale if jam_track_sale sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions # TODO: process shopping_carts_subscriptions
@ -377,7 +378,7 @@ module JamRuby
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed) # this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned # it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
def self.order_jam_tracks(current_user, shopping_carts) def self.order_jam_tracks(current_user, shopping_carts, is_paypal)
shopping_carts_jam_tracks = [] shopping_carts_jam_tracks = []
shopping_carts_subscriptions = [] shopping_carts_subscriptions = []
@ -395,11 +396,9 @@ module JamRuby
end end
end end
client = RecurlyClient.new
sale = nil sale = nil
Sale.transaction do Sale.transaction do
sale = create_jam_track_sale(current_user, SOURCE_RECURLY) sale = create_jam_track_sale(current_user, is_paypal ? SOURCE_PAYPAL : SOURCE_RECURLY)
if sale.valid? if sale.valid?
if is_only_freebie(shopping_carts) if is_only_freebie(shopping_carts)
@ -429,61 +428,142 @@ module JamRuby
else else
account = client.get_account(current_user) if is_paypal
if account.present?
purge_pending_adjustments(account) sale.process_shopping_carts(current_user, shopping_carts)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account) paypal_auth = current_user.paypal_auth
# now invoice the sale ... almost done @api = PayPal::SDK::Merchant::API.new
@get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
@response = @api.get_express_checkout_details(@get_express_checkout_details)
begin @@log.info("User #{current_user.email}, GetExpressCheckout: #{@response.inspect}")
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid tax = false
sale.recurly_invoice_number = invoice.invoice_number if @response.Ack == 'Success'
payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
# we need to ask for taxes
tax = true
end
end
tax_rate = tax ? 0.0825 : 0
total = current_user.shopping_cart_total.round(2)
tax_total = (total * tax_rate).round(2)
total = total + tax_total
total = total.round(2)
@do_express_checkout_payment = @api.build_do_express_checkout_payment({
:DoExpressCheckoutPaymentRequestDetails => {
:PaymentDetails =>
[
{
:OrderTotal => {
:currencyID => "USD",
:value => total
},
:PaymentAction => "Sale"
}
],
:Token => paypal_auth.token,
:PayerID => paypal_auth.uid, }})
@pay_response = @api.do_express_checkout_payment(@do_express_checkout_payment)
@@log.info("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
# #<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseType:0x007fe511dd9b88
# @Timestamp=Sun, 11 Dec 2016 02:09:31 +0000, @Ack="Success",
# @CorrelationID="b28faf6bd90d9", @Version="117.0", @Build="000000",
# @DoExpressCheckoutPaymentResponseDetails=#<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseDetailsType:0x007fe511dd38c8
# @Token="EC-7A4606566T700564B",
# @PaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::PaymentInfoType:0x007fe511dd3008 @TransactionID="63C410710F2619403", @ParentTransactionID=nil,
# @ReceiptID=nil, @TransactionType="express-checkout", @PaymentType="instant", @PaymentDate=Sun, 11 Dec 2016 02:09:31 +0000,
# @GrossAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd0c90 @currencyID="USD", @value="1.99">,
# @FeeAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd04e8 @currencyID="USD", @value="0.36">,
# @TaxAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dcbe98 @currencyID="USD", @value="0.00">,
# @ExchangeRate=nil, @PaymentStatus="Completed", @PendingReason="none", @ReasonCode="none", @ProtectionEligibility="Eligible",
# @ProtectionEligibilityType="ItemNotReceivedEligible,UnauthorizedPaymentEligible", @SellerDetails=#<PayPal::SDK::Merchant::DataTypes::SellerDetailsType:0x007fe511dcb358 @SecureMerchantAccountID="6MB486RSBRMJ2">>],
# @SuccessPageRedirectRequested="false", @CoupledPaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::CoupledPaymentInfoType:0x007fe511dca7a0>]>>
if @pay_response.Ack == 'Success'
details = @pay_response.DoExpressCheckoutPaymentResponseDetails.PaymentInfo[0]
sale.recurly_invoice_id = details.TransactionID
sale.recurly_invoice_number = details.ReceiptID
# now slap in all the real tax/purchase totals # now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents sale.recurly_subtotal_in_cents = ((details.GrossAmount.value.to_f - details.TaxAmount.value.to_f) * 100).to_i
sale.recurly_tax_in_cents = invoice.tax_in_cents sale.recurly_tax_in_cents = (details.TaxAmount.value.to_f * 100).to_i
sale.recurly_total_in_cents = invoice.total_in_cents sale.recurly_total_in_cents = (details.GrossAmount.value.to_f * 100).to_i
sale.recurly_currency = invoice.currency sale.recurly_currency = details.GrossAmount.currencyID
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
end
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
unless sale.save unless sale.save
puts "WTF" puts "Invalid sale (at end)."
raise RecurlyClientError, "Invalid sale (at end)." raise PayPalClientError, "Invalid sale (at end)."
end end
rescue Recurly::Resource::Invalid => e else
# this exception is thrown by invoice! if the invoice is invalid @@log.error("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
sale.rollback_adjustments(current_user, created_adjustments) raise PayPalClientError, @pay_response.Errors[0].LongMessage
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
rescue => e
puts "UNKNOWN E #{e}"
end end
else else
raise RecurlyClientError, "Could not find account to place order." client = RecurlyClient.new
account = client.get_account(current_user)
if account.present?
purge_pending_adjustments(account)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)
# now invoice the sale ... almost done
begin
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid
sale.recurly_invoice_number = invoice.invoice_number
# now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
sale.recurly_tax_in_cents = invoice.tax_in_cents
sale.recurly_total_in_cents = invoice.total_in_cents
sale.recurly_currency = invoice.currency
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
end
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
unless sale.save
puts "Invalid sale (at end)."
raise RecurlyClientError, "Invalid sale (at end)."
end
rescue Recurly::Resource::Invalid => e
# this exception is thrown by invoice! if the invoice is invalid
sale.rollback_adjustments(current_user, created_adjustments)
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
rescue => e
puts "UNKNOWN E #{e}"
end
else
raise RecurlyClientError, "Could not find account to place order."
end
end end
end end
else else
@ -493,7 +573,7 @@ module JamRuby
sale sale
end end
def process_shopping_carts(current_user, shopping_carts, account) def process_shopping_carts(current_user, shopping_carts, account = nil)
created_adjustments = [] created_adjustments = []
@ -515,7 +595,7 @@ module JamRuby
end end
def process_shopping_cart(current_user, shopping_cart, account, created_adjustments) def process_shopping_cart(current_user, shopping_cart, recurly_account, created_adjustments)
recurly_adjustment_uuid = nil recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil recurly_adjustment_credit_uuid = nil
@ -536,14 +616,14 @@ module JamRuby
end end
if account if recurly_account
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack # ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user) adjustments = shopping_cart.create_adjustment_attributes(current_user)
adjustments.each do |adjustment| adjustments.each do |adjustment|
# create the adjustment at Recurly (this may not look like it, but it is a REST API) # create the adjustment at Recurly (this may not look like it, but it is a REST API)
created_adjustment = account.adjustments.new(adjustment) created_adjustment = recurly_account.adjustments.new(adjustment)
created_adjustment.save created_adjustment.save
# if the adjustment could not be made, bail # if the adjustment could not be made, bail

View File

@ -1912,6 +1912,13 @@ module JamRuby
stats stats
end end
def shopping_cart_total
total = 0
shopping_carts.each do |shopping_cart|
total += shopping_cart.product_info[:total_price]
end
total
end
def destroy_all_shopping_carts def destroy_all_shopping_carts
ShoppingCart.where("user_id=?", self).destroy_all ShoppingCart.where("user_id=?", self).destroy_all
end end
@ -2053,6 +2060,15 @@ module JamRuby
user_authorizations.where(provider: "stripe_connect").first user_authorizations.where(provider: "stripe_connect").first
end end
def paypal_auth
user_authorizations.where(provider: 'paypal').first
end
def has_paypal_auth?
auth = paypal_auth
auth && (!auth.token_expiration || auth.token_expiration > Time.now)
end
def has_stripe_connect? def has_stripe_connect?
auth = stripe_auth auth = stripe_auth
auth && (!auth.token_expiration || auth.token_expiration > Time.now) auth && (!auth.token_expiration || auth.token_expiration > Time.now)

View File

@ -28,6 +28,8 @@ gem 'sprockets-rails', '2.3.2'
gem 'non-stupid-digest-assets' gem 'non-stupid-digest-assets'
#gem 'license_finder' #gem 'license_finder'
gem 'pg_migrate', '0.1.14' gem 'pg_migrate', '0.1.14'
#gem 'paypal-sdk-rest'
gem 'paypal-sdk-merchant', github: 'sylv3rblade/merchant-sdk-ruby'
gem 'kickbox' gem 'kickbox'
gem 'oj', '2.10.2' gem 'oj', '2.10.2'
gem 'builder' gem 'builder'

View File

@ -27,7 +27,7 @@
//= require jquery.Jcrop //= require jquery.Jcrop
//= require jquery.naturalsize //= require jquery.naturalsize
//= require jquery.queryparams //= require jquery.queryparams
//= require jquery.clipboard //= require clipboard
//= require jquery.timeago //= require jquery.timeago
//= require jquery.easydropdown //= require jquery.easydropdown
//= require jquery.scrollTo //= require jquery.scrollTo

View File

@ -11,6 +11,7 @@
var userDetail = null; var userDetail = null;
var entity = null; var entity = null;
var remainingCap = 140 - 22 - 1; // 140 tweet max, minus 22 for link size, minus 1 for space var remainingCap = 140 - 22 - 1; // 140 tweet max, minus 22 for link size, minus 1 for space
var clipboard = null;
function showSpinner() { function showSpinner() {
$(dialogId + ' .dialog-inner').hide(); $(dialogId + ' .dialog-inner').hide();
@ -444,27 +445,6 @@
function afterShow() { function afterShow() {
$("#shareType").text(entityType); $("#shareType").text(entityType);
if(context.JK.hasFlash()) {
$("#btn-share-copy").clipboard({
path: '/assets/jquery.clipboard.swf',
copy: function() {
// Return text in closest element (useful when you have multiple boxes that can be copied)
return $(".link-contents").text();
}
});
}
else {
if(context.jamClient) {
// uses bridge call to ultimately access QClipboard
$("#btn-share-copy").unbind('click').click(function() {
context.jamClient.SaveToClipboard($(".link-contents").text());
return false;
})
}
else {
logger.debug("no copy-to-clipboard capabilities")
}
}
} }
function afterHide() { function afterHide() {
@ -486,6 +466,20 @@
//initDialog(); //initDialog();
facebookHelper.deferredLoginStatus().done(function(response) { handleFbStateChange(response); }); facebookHelper.deferredLoginStatus().done(function(response) { handleFbStateChange(response); });
if(context.jamClient.IsNativeClient()) {
$("#btn-share-copy").unbind('click').click(function() {
context.jamClient.SaveToClipboard($("#link-contents").text());
return false;
})
}
else {
clipboard = new Clipboard('#btn-share-copy', {
text: function(trigger) {
return $("#link-contents").text();
}
})
}
} }
this.initialize = initialize; this.initialize = initialize;

View File

@ -2734,6 +2734,27 @@
}) })
} }
function paypalDetail(options) {
options = options || {}
return $.ajax({
type: 'POST',
url: '/api/paypal/checkout/detail',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function paypalPlaceOrder(options) {
options = options || {}
return $.ajax({
type: 'POST',
url: '/api/paypal/checkout/confirm',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function initialize() { function initialize() {
return self; return self;
@ -2977,6 +2998,8 @@
this.posaActivate = posaActivate; this.posaActivate = posaActivate;
this.posaClaim = posaClaim; this.posaClaim = posaClaim;
this.sendRetailerCustomerEmail = sendRetailerCustomerEmail; this.sendRetailerCustomerEmail = sendRetailerCustomerEmail;
this.paypalDetail = paypalDetail;
this.paypalPlaceOrder = paypalPlaceOrder;
return this; return this;
}; };
})(window,jQuery); })(window,jQuery);

View File

@ -9,7 +9,7 @@
//= require jquery.queryparams //= require jquery.queryparams
//= require jquery.hoverIntent //= require jquery.hoverIntent
//= require jquery.cookie //= require jquery.cookie
//= require jquery.clipboard //= require clipboard
//= require jquery.easydropdown //= require jquery.easydropdown
//= require jquery.carousel-1.1 //= require jquery.carousel-1.1
//= require jquery.mousewheel-3.1.9 //= require jquery.mousewheel-3.1.9

View File

@ -213,7 +213,7 @@ MIX_MODES = context.JK.MIX_MODES
.done((response) => .done((response) =>
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count})
) )
.fail(() => .fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable' @app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false}) @setState({searching: false, first_search: false})
) )

View File

@ -326,12 +326,12 @@ MIX_MODES = context.JK.MIX_MODES
.done((response) => .done((response) =>
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count})
) )
.fail(() => .fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable' @app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false}) @setState({searching: false, first_search: false})
) )
) )
.fail(() => .fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable' @app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false}) @setState({searching: false, first_search: false})
) )

View File

@ -0,0 +1,160 @@
context = window
MIX_MODES = context.JK.MIX_MODES
@PayPalConfirmationScreen = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
render: () ->
content = null
if this.state.sold
if context.jamClient && context.jamClient.IsNativeClient()
platformMessage = `<div>
<p> To play your purchased JamTrack, start a session and then open the JamTrack</p>
<a className="download-jamkazam-wrapper" href="/client#/createSession">
<div className="download-jamkazam">
Click Here to Start a Session
</div>
</a>
</div>`
else
platformMessage =
`<div>
<a href='/client#/jamtrack' className="download-jamkazam-wrapper jt-popup">
<div className="download-jamkazam">
Click Here to Start Using Your JamTrack
</div>
</a>
<a href='/downloads' rel="external" className="download-jamkazam-wrapper" target="_blank">
<div>
Do More With Your JamTrack - Click Here to Download Our Application
</div>
</a>
<a className="back-to-browsing" href="/client#/jamtrack">
or click here to browse more jamtracks
</a>
</div>`
content = `<div className="sold-notice">
<h2>Thank you for your order!</h2>
{platformMessage}
</div>`
else
orderButtons = {"button-orange": true, "place-order-btn": true, disabled: this.state.ordering }
cancelButtons = {"button-grey": true, "cancel": true, disabled: this.state.ordering }
content = `<div>
<h2 className="confirm-header">Confirm PayPal Payment</h2>
<p>You have not yet made a payment via PayPal. Please review your purchase and confirm or cancel.</p>
<div className="controls">
<a href="#" className={classNames(orderButtons)} onClick={this.placeOrder}>CONFIRM PURCHASE WITH
PAYPAL</a>
<a href="#" className={classNames(cancelButtons)} onClick={this.cancel}>CANCEL</a>
<div className="clearall"/>
</div>
<ShoppingCartContents carts={this.state.carts}/>
<div className="controls bottom">
<a href="#" className={classNames(orderButtons)} onClick={this.placeOrder}>CONFIRM PURCHASE WITH
PAYPAL</a>
<a href="#" className={classNames(cancelButtons)} onClick={this.cancel}>CANCEL</a>
<div className="clearall"/>
</div>
</div>`
`<div className="PayPalConfirmationScreen">
<div className="content-body-scroller">
{content}
</div>
</div>`
placeOrder: (e) ->
e.preventDefault()
if this.state.ordering
return
@setState({ordering: true})
console.log("placing order with paypal")
@rest.paypalPlaceOrder()
.done((response) =>
console.log("paypal detail obtained", response)
@setState({sold: true, ordering: false})
context.JK.JamTrackUtils.checkShoppingCart()
@app.refreshUser()
)
.fail((jqXHR) =>
@setState({ordering: false})
if jqXHR.status == 404
context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. <a href="/client#/checkoutPayment">Click Here to Checkout Again.</a>')
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message)
else
context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com')
)
cancelOrder: (e) ->
e.preventDefault()
window.location = '/client#/jamtrack'
getInitialState: () ->
{}
componentDidMount: () ->
componentDidUpdate: () ->
afterShow: (data) ->
rest.getShoppingCarts()
.done((carts) =>
@setState({carts: carts})
if carts.length == 0
window.location = '/client#/jamtrack'
return
@rest.paypalDetail()
.done((response) =>
console.log("paypal detail obtained", response)
)
.fail((jqXHR) =>
if jqXHR.status == 404
context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. <a href="/client#/checkoutPayment">Click Here to Checkout Again.</a>')
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message)
else
context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com')
@app.notifyServerError jqXHR, 'PayPal Communication Error'
)
)
.fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Unable to fetch carts'
)
beforeShow: () ->
this.setState({sold: false})
onAppInit: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
screenBindings =
'beforeShow': @beforeShow
'afterShow': @afterShow
@app.bindScreen('paypal/confirm', screenBindings)
onUserChanged: (userState) ->
@user = userState?.user
})

View File

@ -0,0 +1,106 @@
context = window
MIX_MODES = context.JK.MIX_MODES
@ShoppingCartContents = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
render: () ->
carts = []
if this.props.carts?
if this.props.carts.length == 0
carts = `<div className="no-cart-items">You have nothing in your cart</div>`
else
taxRate = 0
if this.props.tax
taxRate = 0.0825
estimatedTax = 0
estimatedTotal = 0
for cart in this.props.carts
cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem
estimatedTax += cart.product_info.price * cart_quantity * taxRate
estimatedTotal += cart.product_info.price * cart_quantity
estimatedTax = Math.round(estimatedTax * 100) / 100
estimatedTotal = Math.round((estimatedTotal + estimatedTax) * 100) / 100
for cart in this.props.carts
console.log("CART", cart)
freeNotice = null
if cart.product_info.free
freeNotice = `<span className="first-one-free">| (first one free)</span>`
carts.push(`<div className="cart-item" key={cart.id}>
<div className="cart-item-caption">
<span>{cart.product_info.sale_display}</span>
{freeNotice}
</div>
<div className="cart-item-price">
$ {Number(cart.product_info.real_price).toFixed(2)}
</div>
<div className="cart-item-quantity">
{cart.quantity}
</div>
<div className="clearall"/>
</div>`)
carts.push(`<div className="cart-item tax-total" key={'tax'}>
<div className="cart-item-caption">
<span>Tax</span>
</div>
<div className="cart-item-price">
$ {estimatedTax.toFixed(2)}
</div>
<div className="cart-item-quantity">
</div>
<div className="clearall"/>
</div>`)
carts.push(`<div className="cart-item total" key={'total'}>
<div className="cart-item-caption">
<span>Total</span>
</div>
<div className="cart-item-price">
$ {estimatedTotal.toFixed(2)}
</div>
<div className="cart-item-quantity">
</div>
<div className="clearall"/>
</div>`)
else
carts = `<div className="loading-indicator">Loading...</div>`
`<div className="shopping-cart-contents">
<div className="order-items-page">
<div className="cart-items">
<div className="cart-item-caption">
<span>YOUR ORDER INCLUDES:</span>
</div>
<div className="cart-item-price">
<span>PRICE</span>
</div>
<div className="cart-item-quantity">
<span>QUANTITY</span>
</div>
<div className="clearall"></div>
{carts}
<div className="clearall"></div>
</div>
</div>
</div>`
onAppInit: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
onUserChanged: (userState) ->
@user = userState?.user
})

View File

@ -43,6 +43,7 @@ proficiencyDescriptionMap = {
TILE_RATINGS: 'ratings' TILE_RATINGS: 'ratings'
TILE_PRICES: 'prices' TILE_PRICES: 'prices'
visible: false visible: false
profileClipboard: null
TILES: ['about', 'experience', 'samples', 'ratings', 'prices'] TILES: ['about', 'experience', 'samples', 'ratings', 'prices']
@ -65,11 +66,26 @@ proficiencyDescriptionMap = {
@root = $(@getDOMNode()) @root = $(@getDOMNode())
@screen = $('#teacher-profile') @screen = $('#teacher-profile')
@starbox() @starbox()
@clipboard()
componentDidUpdate:() -> componentDidUpdate:() ->
@starbox() @starbox()
context.JK.popExternalLinks(@root) context.JK.popExternalLinks(@root)
@clipboard()
clipboard: () ->
$profileLink = @root.find('.copy-profile-link')
if $profileLink.length > 0 && !@profileClipboard?
# mount it
@profileClipboard = new Clipboard($profileLink.get(0), {
text: =>
return context.JK.makeAbsolute('/client#/teacher/profile/' + @state.user.teacher?.id)
})
else if $profileLink.length == 0 && @profileClipboard?
@profileClipboard.destroy()
@profileClipboard = null
starbox:() -> starbox:() ->
$ratings = @root.find('.ratings-box') $ratings = @root.find('.ratings-box')
@ -230,7 +246,9 @@ proficiencyDescriptionMap = {
biography = biography.replace(/\n/g, "<br/>") biography = biography.replace(/\n/g, "<br/>")
`<div className="section bio"> `<div className="section bio">
<h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3> <a className="copy-profile-link button-orange" href='#' onClick={this.copyProfileLink}>
COPY PROFILE URL TO CLIPBOARD
</a><h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3>
<div className="section-content"> <div className="section-content">
<div dangerouslySetInnerHTML={{__html: biography}}></div> <div dangerouslySetInnerHTML={{__html: biography}}></div>
</div> </div>
@ -694,6 +712,16 @@ proficiencyDescriptionMap = {
</div> </div>
</div>` </div>`
copyProfileLink: (e) ->
e.preventDefault()
@app.layout.notify({
title: 'Teacher Profile Link Copied',
text: "Your clipboard now has a link to this teacher that you can share with anyone."
})
selectionMade: (selection, e) -> selectionMade: (selection, e) ->
e.preventDefault() e.preventDefault()

View File

@ -102,7 +102,7 @@
var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false } var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false }
$self.triggerHandler('startedRecording', details); $self.triggerHandler('startedRecording', details);
currentlyRecording = false; currentlyRecording = false;
context.RecordingActions.startedRecording(details); context.RecordingActions.startedRecording(details);
}) })

View File

@ -9,7 +9,7 @@
//= require jquery.queryparams //= require jquery.queryparams
//= require jquery.hoverIntent //= require jquery.hoverIntent
//= require jquery.cookie //= require jquery.cookie
//= require jquery.clipboard //= require clipboard
//= require jquery.easydropdown //= require jquery.easydropdown
//= require jquery.carousel-1.1 //= require jquery.carousel-1.1
//= require jquery.mousewheel-3.1.9 //= require jquery.mousewheel-3.1.9

View File

@ -33,6 +33,24 @@
} }
} }
.paypal-region {
text-align: center;
margin:10px auto 0;
/**margin: 10px auto 0;
padding: 10px 10px 5px;
background-color: white;
border-radius: 8px;
border-color: #ccc;
border-style: solid;
border-width: 3px;
width:145px;*/
}
.or-text {
margin: 60px auto 0;
text-align:center;
}
h2 { h2 {
color:white; color:white;
background-color:#4d4d4d; background-color:#4d4d4d;

View File

@ -0,0 +1,78 @@
@import "client/common.scss";
[data-react-class="PayPalConfirmationScreen"] {
height: 100%;
overflow: scroll;
.content-body-scroller {
height: calc(100% - 30px) ! important; // 15px top and bottom padding, and 48px used by .controls
padding: 15px 30px;
}
.confirm-header {
color: white;
font-size: 20px;
}
.controls.bottom {
margin-top: 20px;
}
.place-order-btn {
text-align: center;
margin-right:0;
}
.or-holder {
margin-top: 20px;
text-align: center;
}
.cancel-order-btn {
margin-top: 20px;
text-align: center;
}
.shopping-cart-contents {
@include border-box_sizing;
width: 50%;
margin-top:20px;
}
.controls {
@include border-box_sizing;
width:50%;
a {
float:right;
}
}
.loading-indicator {
margin-bottom:20px;
padding-bottom:20px;
}
.sold-notice {
h2 {
font-size:30px;
text-align:center;
}
}
.download-jamkazam {
color:$ColorLink;
border-radius: 4px;
border-style:solid;
border-color:#AAA;
border-width:1px;
padding:10px;
margin-top:20px;
display:inline-block;
}
.download-jamkazam-wrapper, .back-to-browsing {
text-align:center;
display:block;
margin-top:35px;
&.hidden {
display:none;
}
}
}

View File

@ -0,0 +1,64 @@
@import "client/common.scss";
.shopping-cart-contents {
background-color:#262626;
border-width:0 1px 0 0;
border-style:solid;
border-color:#333;
padding:20px 20px 0;
.cart-item-caption {
width: 50%;
text-align: left;
float: left;
margin-bottom: 10px;
@include border_box_sizing;
}
.first-one-free {
font-size: 14px;
font-style: italic;
margin-left: 15px;
}
.cart-item-price {
width: 25%;
text-align: right;
float: left;
padding: 0 10px;
margin-bottom: 10px;
@include border_box_sizing;
}
.cart-item-quantity {
width: 10%;
text-align: right;
float: left;
padding: 0 10px;
margin-bottom: 10px;
@include border_box_sizing;
}
.cart-items {
margin-top: 10px;
padding-left: 10px;
}
.cart-item {
margin-top: 10px;
}
.no-cart-items {
}
.tax-total {
margin-top:10px;
border-width:1px 0 0 0;
border-color:white;
border-style:solid;
padding-top:10px;
}
.cart-item.total {
margin-top:5px;
}
}

View File

@ -199,6 +199,9 @@
position:absolute; position:absolute;
} }
.copy-profile-link {
float:right;
}
.spinner-large { .spinner-large {
width:200px; width:200px;

View File

@ -15,7 +15,6 @@ class ApiLessonSessionsController < ApiController
render "api_lesson_sessions/index", :layout => nil render "api_lesson_sessions/index", :layout => nil
end end
def show def show
end end
@ -72,13 +71,13 @@ class ApiLessonSessionsController < ApiController
if params[:update_all] if params[:update_all]
# check if the next scheduled lesson is doable # check if the next scheduled lesson is doable
if 24.hours.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start if 15.minutes.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start
response = {message: 'time_limit'} response = {message: 'time_limit'}
render :json => response, :status => 422 render :json => response, :status => 422
return return
end end
else else
if 24.hours.from_now > @lesson_session.music_session.scheduled_start if 15.minutes.from_now > @lesson_session.music_session.scheduled_start
response = {message: 'time_limit'} response = {message: 'time_limit'}
render :json => response, :status => 422 render :json => response, :status => 422
return return

View File

@ -0,0 +1,136 @@
class ApiPayPalController < ApiController
before_filter :api_signed_in_user
respond_to :json
def log
@log || Logging.logger[VanillaForumsController]
end
def start_checkout
cancel_path = params[:path] ? params[:path] : ERB::Util.url_encode('/client#/checkoutPayment')
tax = true
tax_rate = tax ? 0.0825 : 0
total = current_user.shopping_cart_total.round(2)
tax_total = (total * tax_rate).round(2)
total = total + tax_total
total = total.round(2)
@api = PayPal::SDK::Merchant::API.new
@set_express_checkout = @api.build_set_express_checkout(
{
:Version => "117.0",
:SetExpressCheckoutRequestDetails =>
{
:ReturnURL => ApplicationHelper.base_uri(request) + '/auth/paypal/checkout',
:CancelURL => ApplicationHelper.base_uri(request) + '/auth/paypal/checkout?cancel=1&path=' + cancel_path,
# :NoShipping => "1",
# :ReqConfirmShipping => "0",
# :ReqBillingAddress => "1",
:PaymentDetails =>
[
{
:OrderTotal => {
:currencyID => "USD",
:value => total
},
:PaymentAction => "Sale"
}
]
}
}
)
@set_express_checkout_response = @api.set_express_checkout(@set_express_checkout)
log.info("User #{current_user.email}, SetExpressCheckout #{@set_express_checkout_response.inspect}")
if @set_express_checkout_response.Ack == 'Failure'
render json: {message: @set_express_checkout_response.Errors[0].LongMessage}, status: 422
return
end
redirect_to Rails.configuration.paypal_express_url + '&token=' + ERB::Util.url_encode(@set_express_checkout_response.Token)
end
# called by frontend after the user comes back from initial express page
def checkout_detail
# here we can see if they will pay tax
if !current_user.has_paypal_auth?
render json: {}, :status => 404
return
end
paypal_auth = current_user.paypal_auth
@api = PayPal::SDK::Merchant::API.new
@get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
@response = @api.get_express_checkout_details(@get_express_checkout_details)
puts @response.inspect
tax = false
if @response.Ack == 'Success'
payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
# we need to ask for taxes
tax = true
end
else
render json: {message: @response.Errors[0].LongMessage}, status: 422
return
end
log.debug("User #{current_user.email}, GetExpressCheckout: #{@get_express_checkout_details_response.inspect}")
render json: {tax: tax}
end
# called by frontend when the user selects finally 'confirm purchase' (PLACE ORDER btn)
def confirm_purchase
if !current_user.has_paypal_auth?
render json: {}, :status => 404
return
end
error = nil
response = {jam_tracks: [], gift_cards: []}
#if Sale.is_mixed(current_user.shopping_carts)
# msg = "has free and non-free items. Try removing non-free items."
# render json: {message: "Cart " + msg, errors: {cart: [msg]}}, :status => 404
# return
#end
begin
sales = Sale.place_order(current_user, current_user.shopping_carts, true)
rescue RecurlyClientError => e
render json: {message: e.errors[:message]}, :status => 422
return
rescue PayPalClientError => x
render json: {message: x.errors[:message]}, :status => 422
return
end
sales.each do |sale|
sale.sale_line_items.each do |line_item|
if line_item.is_jam_track?
jam_track = line_item.product
jam_track_right = jam_track.right_for_user(current_user)
response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version}
elsif line_item.is_gift_card?
gift_card = line_item.product
response[:gift_cards] << {name: gift_card.name, id: gift_card.id}
else
raise 'unknown sale line item type: ' + line_item.product_type
end
end
end
set_purchased_jamtrack_cookie
render :json => response, :status => 200
end
end

View File

@ -1,6 +1,8 @@
# this is not a jam session - this is an 'auth session' # this is not a jam session - this is an 'auth session'
class SessionsController < ApplicationController class SessionsController < ApplicationController
before_filter :api_signed_in_user, only: :paypal_express_checkout
layout "web" layout "web"
def signin def signin
@ -37,6 +39,42 @@ class SessionsController < ApplicationController
end end
end end
def paypal_express_checkout
# should get 'token' and 'PayerID' on success
# on failure, cancel=1
if params[:cancel] == '1' || params[:cancel] == 1
redirect_to params[:path] ? params[:path] : '/client#/jamtrack'
return
end
authorization = current_user.paypal_auth
# Always make and save a new authorization. This is because they expire, and honestly there's no cost
# to just making and saving it.
user_auth_hash = {
:provider => 'paypal',
:uid => params[:PayerID],
:token => params[:token],
:refresh_token => nil,
:token_expiration => 3.hours.from_now, # according to paypal docs, a token is good for 3 hours
:secret => nil
}
if authorization.nil?
authorization = current_user.user_authorizations.build(user_auth_hash)
authorization.save
else
authorization.token = user_auth_hash[:token]
authorization.token_expiration = user_auth_hash[:token_expiration]
authorization.uid = user_auth_hash[:uid]
authorization.save
end
redirect_to '/client#/paypal/confirm'
end
# OAuth docs # OAuth docs
# http://net.tutsplus.com/tutorials/ruby/how-to-use-omniauth-to-authenticate-your-users/ # http://net.tutsplus.com/tutorials/ruby/how-to-use-omniauth-to-authenticate-your-users/

View File

@ -120,6 +120,13 @@ div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class
.divSaveCardHelper .divSaveCardHelper
label for="save-card" Save card for future use label for="save-card" Save card for future use
.clearall .clearall
- if !Rails.application.config.paypal_admin_only || any_user.admin
.or-text or instead use:
.paypal-region
a href="/paypal/checkout/start" data-paypal-button="true"
img src="https://www.paypalobjects.com/webstatic/en_US/i/btn/png/gold-pill-paypalcheckout-34px.png" alt="PayPal Checkout"
a
.clearall .clearall
.clearall .clearall
.row.second .row.second

View File

@ -0,0 +1,8 @@
.screen.secondary layout='screen' layout-id='paypal/confirm'
.content
.content-head
.content-icon=image_tag("content/icon_jamtracks.png", height: 19, width: 19)
h1 confirm payment
= render "screen_navigation"
.content-body
= react_component 'PayPalConfirmationScreen', {}

View File

@ -58,6 +58,7 @@
<%= render "jamtrack_search" %> <%= render "jamtrack_search" %>
<%= render "jamtrack_filter" %> <%= render "jamtrack_filter" %>
<%= render "jamtrack_landing" %> <%= render "jamtrack_landing" %>
<%= render "paypal_confirmation" %>
<%= render "shopping_cart" %> <%= render "shopping_cart" %>
<%= render "checkout_signin" %> <%= render "checkout_signin" %>
<%= render "checkout_payment" %> <%= render "checkout_payment" %>

View File

@ -38,7 +38,7 @@
<div class="share-link border-bottom"> <div class="share-link border-bottom">
<h3>Share a Link:</h3> <h3>Share a Link:</h3>
<div class="link-contents"> <div class="link-contents" id="link-contents">
</div> </div>
<div class="right"><a id="btn-share-copy" class="button-orange">COPY LINK</a></div> <div class="right"><a id="btn-share-copy" class="button-orange">COPY LINK</a></div>

View File

@ -452,6 +452,17 @@ if defined?(Bundler)
# This would transparently migrate your existing Marshal-serialized cookies into the new JSON-based format. # This would transparently migrate your existing Marshal-serialized cookies into the new JSON-based format.
config.action_dispatch.cookies_serializer = :hybrid config.action_dispatch.cookies_serializer = :hybrid
config.jam_class_card_wait_period_year = 1 config.jam_class_card_wait_period_year = 1
config.paypal_mode = 'sandbox' #sandbox or live
config.app_id = 'APP-80W284485P519543T' # this is constant across all sandbox accts
config.paypal_username = 'seth+ppmerchant_api1.jamkazam.com' # seth+ppmerchant@jamkazam.com' # 'seth+ppmerchant_api1.jamkazam.com'
config.paypal_client_id = 'AZ5CCd8lHNntZ0ddxk_Wvo6LUaACd-bsMr7OPu_M1oI9vPN3d89mdSKswns9GEKB57qIwTT9_NyyK1c8'
config.paypal_client_secret = 'EIK0KWr8G5ntzYyJzbGCrsdr62aaJw2PdtXJrQddI9WuraBxJfiL4aMtKWu7Cyo4ACt13hLvCqg84HjP'
config.paypal_password = 'UXDKMPLYNE5YRLMK'
config.paypal_signature = 'AFcWxV21C7fd0v3bYYYRCpSSRl31AmvhBjN40M8etif4EA.L8EyMxdsu'
config.paypal_debug = true
config.paypal_express_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout' # drop sandbox for production
config.paypal_admin_only = true
end end
end end

View File

@ -0,0 +1,13 @@
PayPal::SDK.configure(
:mode => Rails.configuration.paypal_mode,
:username => Rails.configuration.paypal_username,
:password => Rails.configuration.paypal_password,
:signature => Rails.configuration.paypal_signature,
:ssl_options => { } )
# :client_id => Rails.configuration.paypal_client_id,
# :client_secret => Rails.configuration.paypal_client_secret,
PayPal::SDK.logger = Logging.logger['PayPal']
PayPal::SDK.logger.level = Rails.configuration.paypal_debug ? Logger::DEBUG : Logger::INFO

View File

@ -77,6 +77,7 @@ Rails.application.routes.draw do
get '/auth/:provider/callback', :to => 'sessions#oauth_callback' get '/auth/:provider/callback', :to => 'sessions#oauth_callback'
get '/auth/failure', :to => 'sessions#failure' get '/auth/failure', :to => 'sessions#failure'
get '/auth/has_google_auth', :to => 'sessions#has_google_auth' get '/auth/has_google_auth', :to => 'sessions#has_google_auth'
get '/auth/paypal/checkout', :to => 'sessions#paypal_express_checkout'
# session info page # session info page
get '/sessions/:id/details' => 'music_sessions#session_info', :as => 'music_scheduled_session_detail' get '/sessions/:id/details' => 'music_sessions#session_info', :as => 'music_scheduled_session_detail'
@ -125,6 +126,8 @@ Rails.application.routes.draw do
get '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse' get '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse'
get '/paypal/checkout/start' => 'api_pay_pal#start_checkout'
# embed resque-web if this is development mode # embed resque-web if this is development mode
if Rails.env == "development" || Rails.application.config.allow_spikes if Rails.env == "development" || Rails.application.config.allow_spikes
require 'resque/server' require 'resque/server'
@ -260,7 +263,6 @@ Rails.application.routes.draw do
match '/music_notations/:id' => 'api_music_notations#download', :via => :get, :as => :download_music_notation match '/music_notations/:id' => 'api_music_notations#download', :via => :get, :as => :download_music_notation
match '/music_notations/:id' => 'api_music_notations#delete', :via => :delete, :as => :delete_music_notation match '/music_notations/:id' => 'api_music_notations#delete', :via => :delete, :as => :delete_music_notation
# Backing track_show # Backing track_show
match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list' match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list'
@ -363,6 +365,10 @@ Rails.application.routes.draw do
match '/recurly/place_order' => 'api_recurly#place_order', :via => :post match '/recurly/place_order' => 'api_recurly#place_order', :via => :post
match '/ios/order_placed' => 'api_jam_tracks#ios_order_placed', :via => :post match '/ios/order_placed' => 'api_jam_tracks#ios_order_placed', :via => :post
# paypal
match '/paypal/checkout/detail' => 'api_pay_pal#checkout_detail', :via => :post
match '/paypal/checkout/confirm' => 'api_pay_pal#confirm_purchase', :via => :post
# sale info # sale info
match '/payment_histories' => 'api_payment_histories#index', :via => :get match '/payment_histories' => 'api_payment_histories#index', :via => :get

View File

@ -0,0 +1,753 @@
/*!
* clipboard.js v1.5.15
* https://zenorocha.github.io/clipboard.js
*
* Licensed MIT © Zeno Rocha
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* A polyfill for Element.matches()
*/
if (Element && !Element.prototype.matches) {
var proto = Element.prototype;
proto.matches = proto.matchesSelector ||
proto.mozMatchesSelector ||
proto.msMatchesSelector ||
proto.oMatchesSelector ||
proto.webkitMatchesSelector;
}
/**
* Finds the closest parent that matches a selector.
*
* @param {Element} element
* @param {String} selector
* @return {Function}
*/
function closest (element, selector) {
while (element && element !== document) {
if (element.matches(selector)) return element;
element = element.parentNode;
}
}
module.exports = closest;
},{}],2:[function(require,module,exports){
var closest = require('./closest');
/**
* Delegates event to a selector.
*
* @param {Element} element
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @param {Boolean} useCapture
* @return {Object}
*/
function delegate(element, selector, type, callback, useCapture) {
var listenerFn = listener.apply(this, arguments);
element.addEventListener(type, listenerFn, useCapture);
return {
destroy: function() {
element.removeEventListener(type, listenerFn, useCapture);
}
}
}
/**
* Finds closest match and invokes callback.
*
* @param {Element} element
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Function}
*/
function listener(element, selector, type, callback) {
return function(e) {
e.delegateTarget = closest(e.target, selector);
if (e.delegateTarget) {
callback.call(element, e);
}
}
}
module.exports = delegate;
},{"./closest":1}],3:[function(require,module,exports){
/**
* Check if argument is a HTML element.
*
* @param {Object} value
* @return {Boolean}
*/
exports.node = function(value) {
return value !== undefined
&& value instanceof HTMLElement
&& value.nodeType === 1;
};
/**
* Check if argument is a list of HTML elements.
*
* @param {Object} value
* @return {Boolean}
*/
exports.nodeList = function(value) {
var type = Object.prototype.toString.call(value);
return value !== undefined
&& (type === '[object NodeList]' || type === '[object HTMLCollection]')
&& ('length' in value)
&& (value.length === 0 || exports.node(value[0]));
};
/**
* Check if argument is a string.
*
* @param {Object} value
* @return {Boolean}
*/
exports.string = function(value) {
return typeof value === 'string'
|| value instanceof String;
};
/**
* Check if argument is a function.
*
* @param {Object} value
* @return {Boolean}
*/
exports.fn = function(value) {
var type = Object.prototype.toString.call(value);
return type === '[object Function]';
};
},{}],4:[function(require,module,exports){
var is = require('./is');
var delegate = require('delegate');
/**
* Validates all params and calls the right
* listener function based on its target type.
*
* @param {String|HTMLElement|HTMLCollection|NodeList} target
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listen(target, type, callback) {
if (!target && !type && !callback) {
throw new Error('Missing required arguments');
}
if (!is.string(type)) {
throw new TypeError('Second argument must be a String');
}
if (!is.fn(callback)) {
throw new TypeError('Third argument must be a Function');
}
if (is.node(target)) {
return listenNode(target, type, callback);
}
else if (is.nodeList(target)) {
return listenNodeList(target, type, callback);
}
else if (is.string(target)) {
return listenSelector(target, type, callback);
}
else {
throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');
}
}
/**
* Adds an event listener to a HTML element
* and returns a remove listener function.
*
* @param {HTMLElement} node
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenNode(node, type, callback) {
node.addEventListener(type, callback);
return {
destroy: function() {
node.removeEventListener(type, callback);
}
}
}
/**
* Add an event listener to a list of HTML elements
* and returns a remove listener function.
*
* @param {NodeList|HTMLCollection} nodeList
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenNodeList(nodeList, type, callback) {
Array.prototype.forEach.call(nodeList, function(node) {
node.addEventListener(type, callback);
});
return {
destroy: function() {
Array.prototype.forEach.call(nodeList, function(node) {
node.removeEventListener(type, callback);
});
}
}
}
/**
* Add an event listener to a selector
* and returns a remove listener function.
*
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenSelector(selector, type, callback) {
return delegate(document.body, selector, type, callback);
}
module.exports = listen;
},{"./is":3,"delegate":2}],5:[function(require,module,exports){
function select(element) {
var selectedText;
if (element.nodeName === 'SELECT') {
element.focus();
selectedText = element.value;
}
else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
element.focus();
element.setSelectionRange(0, element.value.length);
selectedText = element.value;
}
else {
if (element.hasAttribute('contenteditable')) {
element.focus();
}
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
selectedText = selection.toString();
}
return selectedText;
}
module.exports = select;
},{}],6:[function(require,module,exports){
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
},{}],7:[function(require,module,exports){
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['module', 'select'], factory);
} else if (typeof exports !== "undefined") {
factory(module, require('select'));
} else {
var mod = {
exports: {}
};
factory(mod, global.select);
global.clipboardAction = mod.exports;
}
})(this, function (module, _select) {
'use strict';
var _select2 = _interopRequireDefault(_select);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var ClipboardAction = function () {
/**
* @param {Object} options
*/
function ClipboardAction(options) {
_classCallCheck(this, ClipboardAction);
this.resolveOptions(options);
this.initSelection();
}
/**
* Defines base properties passed from constructor.
* @param {Object} options
*/
_createClass(ClipboardAction, [{
key: 'resolveOptions',
value: function resolveOptions() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.action = options.action;
this.emitter = options.emitter;
this.target = options.target;
this.text = options.text;
this.trigger = options.trigger;
this.selectedText = '';
}
}, {
key: 'initSelection',
value: function initSelection() {
if (this.text) {
this.selectFake();
} else if (this.target) {
this.selectTarget();
}
}
}, {
key: 'selectFake',
value: function selectFake() {
var _this = this;
var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
this.removeFake();
this.fakeHandlerCallback = function () {
return _this.removeFake();
};
this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true;
this.fakeElem = document.createElement('textarea');
// Prevent zooming on iOS
this.fakeElem.style.fontSize = '12pt';
// Reset box model
this.fakeElem.style.border = '0';
this.fakeElem.style.padding = '0';
this.fakeElem.style.margin = '0';
// Move element out of screen horizontally
this.fakeElem.style.position = 'absolute';
this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
// Move element to the same position vertically
var yPosition = window.pageYOffset || document.documentElement.scrollTop;
this.fakeElem.addEventListener('focus', window.scrollTo(0, yPosition));
this.fakeElem.style.top = yPosition + 'px';
this.fakeElem.setAttribute('readonly', '');
this.fakeElem.value = this.text;
document.body.appendChild(this.fakeElem);
this.selectedText = (0, _select2.default)(this.fakeElem);
this.copyText();
}
}, {
key: 'removeFake',
value: function removeFake() {
if (this.fakeHandler) {
document.body.removeEventListener('click', this.fakeHandlerCallback);
this.fakeHandler = null;
this.fakeHandlerCallback = null;
}
if (this.fakeElem) {
document.body.removeChild(this.fakeElem);
this.fakeElem = null;
}
}
}, {
key: 'selectTarget',
value: function selectTarget() {
this.selectedText = (0, _select2.default)(this.target);
this.copyText();
}
}, {
key: 'copyText',
value: function copyText() {
var succeeded = void 0;
try {
succeeded = document.execCommand(this.action);
} catch (err) {
succeeded = false;
}
this.handleResult(succeeded);
}
}, {
key: 'handleResult',
value: function handleResult(succeeded) {
this.emitter.emit(succeeded ? 'success' : 'error', {
action: this.action,
text: this.selectedText,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
}
}, {
key: 'clearSelection',
value: function clearSelection() {
if (this.target) {
this.target.blur();
}
window.getSelection().removeAllRanges();
}
}, {
key: 'destroy',
value: function destroy() {
this.removeFake();
}
}, {
key: 'action',
set: function set() {
var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy';
this._action = action;
if (this._action !== 'copy' && this._action !== 'cut') {
throw new Error('Invalid "action" value, use either "copy" or "cut"');
}
},
get: function get() {
return this._action;
}
}, {
key: 'target',
set: function set(target) {
if (target !== undefined) {
if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) {
if (this.action === 'copy' && target.hasAttribute('disabled')) {
throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
}
if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
}
this._target = target;
} else {
throw new Error('Invalid "target" value, use a valid Element');
}
}
},
get: function get() {
return this._target;
}
}]);
return ClipboardAction;
}();
module.exports = ClipboardAction;
});
},{"select":5}],8:[function(require,module,exports){
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['module', './clipboard-action', 'tiny-emitter', 'good-listener'], factory);
} else if (typeof exports !== "undefined") {
factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener'));
} else {
var mod = {
exports: {}
};
factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener);
global.clipboard = mod.exports;
}
})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) {
'use strict';
var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
var _goodListener2 = _interopRequireDefault(_goodListener);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Clipboard = function (_Emitter) {
_inherits(Clipboard, _Emitter);
/**
* @param {String|HTMLElement|HTMLCollection|NodeList} trigger
* @param {Object} options
*/
function Clipboard(trigger, options) {
_classCallCheck(this, Clipboard);
var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this));
_this.resolveOptions(options);
_this.listenClick(trigger);
return _this;
}
/**
* Defines if attributes would be resolved using internal setter functions
* or custom functions that were passed in the constructor.
* @param {Object} options
*/
_createClass(Clipboard, [{
key: 'resolveOptions',
value: function resolveOptions() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
this.text = typeof options.text === 'function' ? options.text : this.defaultText;
}
}, {
key: 'listenClick',
value: function listenClick(trigger) {
var _this2 = this;
this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) {
return _this2.onClick(e);
});
}
}, {
key: 'onClick',
value: function onClick(e) {
var trigger = e.delegateTarget || e.currentTarget;
if (this.clipboardAction) {
this.clipboardAction = null;
}
this.clipboardAction = new _clipboardAction2.default({
action: this.action(trigger),
target: this.target(trigger),
text: this.text(trigger),
trigger: trigger,
emitter: this
});
}
}, {
key: 'defaultAction',
value: function defaultAction(trigger) {
return getAttributeValue('action', trigger);
}
}, {
key: 'defaultTarget',
value: function defaultTarget(trigger) {
var selector = getAttributeValue('target', trigger);
if (selector) {
return document.querySelector(selector);
}
}
}, {
key: 'defaultText',
value: function defaultText(trigger) {
return getAttributeValue('text', trigger);
}
}, {
key: 'destroy',
value: function destroy() {
this.listener.destroy();
if (this.clipboardAction) {
this.clipboardAction.destroy();
this.clipboardAction = null;
}
}
}]);
return Clipboard;
}(_tinyEmitter2.default);
/**
* Helper function to retrieve attribute value.
* @param {String} suffix
* @param {Element} element
*/
function getAttributeValue(suffix, element) {
var attribute = 'data-clipboard-' + suffix;
if (!element.hasAttribute(attribute)) {
return;
}
return element.getAttribute(attribute);
}
module.exports = Clipboard;
});
},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)
});

View File

@ -1,611 +0,0 @@
/*
* jQuery Clipboard :: Fork of zClip :: Uses ZeroClipboard v1.2.3
*
* https://github.com/valeriansaliou/jquery.clipboard
* http://www.steamdev.com/zclip/
*
* Copyright 2013, Valérian Saliou
* Copyright 2011, SteamDev
*
* Released under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*
* Version: v1.2
* Date: Sun Dec 1, 2013
*/
/* Component: jQuery Clipboard */
(function ($) {
var $clip = null;
var $is_loaded = false;
$.fn.clipboard = function (params) {
if ((typeof params == 'object' && !params.length) || (typeof params == 'undefined')) {
var settings = $.extend({
path: 'jquery.clipboard.swf',
copy: null,
beforeCopy: null,
afterCopy: null,
clickAfter: true
}, (params || {}));
return this.each(function () {
var o = $(this);
if (o.is(':visible') && (typeof settings.copy == 'string' || $.isFunction(settings.copy))) {
if ($.isFunction(settings.copy)) {
o.bind('Clipboard_copy',settings.copy);
}
if ($.isFunction(settings.beforeCopy)) {
o.bind('Clipboard_beforeCopy',settings.beforeCopy);
}
if ($.isFunction(settings.afterCopy)) {
o.bind('Clipboard_afterCopy',settings.afterCopy);
}
if($clip === null) {
$clip = new ZeroClipboard(null, {
moviePath: settings.path,
trustedDomains: '*',
hoverClass: 'hover',
activeClass: 'active'
});
$clip.on('load', function(client) {
client.on('mouseover', function (client) {
$(this).trigger('mouseenter');
});
client.on('mouseout', function (client) {
$(this).trigger('mouseleave');
});
client.on('mousedown', function (client) {
$(this).trigger('mousedown');
if (!$.isFunction(settings.copy)) {
client.setText(settings.copy);
} else {
client.setText($(this).triggerHandler('Clipboard_copy'));
}
if ($.isFunction(settings.beforeCopy)) {
$(this).trigger('Clipboard_beforeCopy');
}
});
client.on('complete', function (client, args) {
if ($.isFunction(settings.afterCopy)) {
$(this).trigger('Clipboard_afterCopy');
} else {
$(this).removeClass('hover');
}
if (settings.clickAfter) {
$(this).trigger('click');
}
});
});
}
$clip.glue(o[0]);
}
});
}
};
})(jQuery);
/* Component: ZeroClipboard */
(function() {
"use strict";
var _camelizeCssPropName = function() {
var matcherRegex = /\-([a-z])/g, replacerFn = function(match, group) {
return group.toUpperCase();
};
return function(prop) {
return prop.replace(matcherRegex, replacerFn);
};
}();
var _getStyle = function(el, prop) {
var value, camelProp, tagName, possiblePointers, i, len;
if (window.getComputedStyle) {
value = window.getComputedStyle(el, null).getPropertyValue(prop);
} else {
camelProp = _camelizeCssPropName(prop);
if (el.currentStyle) {
value = el.currentStyle[camelProp];
} else {
value = el.style[camelProp];
}
}
if (prop === "cursor") {
if (!value || value === "auto") {
tagName = el.tagName.toLowerCase();
possiblePointers = [ "a" ];
for (i = 0, len = possiblePointers.length; i < len; i++) {
if (tagName === possiblePointers[i]) {
return "pointer";
}
}
}
}
return value;
};
var _elementMouseOver = function(event) {
if (!ZeroClipboard.prototype._singleton) return;
if (!event) {
event = window.event;
}
var target;
if (this !== window) {
target = this;
} else if (event.target) {
target = event.target;
} else if (event.srcElement) {
target = event.srcElement;
}
ZeroClipboard.prototype._singleton.setCurrent(target);
};
var _addEventHandler = function(element, method, func) {
if (element.addEventListener) {
element.addEventListener(method, func, false);
} else if (element.attachEvent) {
element.attachEvent("on" + method, func);
}
};
var _removeEventHandler = function(element, method, func) {
if (element.removeEventListener) {
element.removeEventListener(method, func, false);
} else if (element.detachEvent) {
element.detachEvent("on" + method, func);
}
};
var _addClass = function(element, value) {
if (element.addClass) {
element.addClass(value);
return element;
}
if (value && typeof value === "string") {
var classNames = (value || "").split(/\s+/);
if (element.nodeType === 1) {
if (!element.className) {
element.className = value;
} else {
var className = " " + element.className + " ", setClass = element.className;
for (var c = 0, cl = classNames.length; c < cl; c++) {
if (className.indexOf(" " + classNames[c] + " ") < 0) {
setClass += " " + classNames[c];
}
}
element.className = setClass.replace(/^\s+|\s+$/g, "");
}
}
}
return element;
};
var _removeClass = function(element, value) {
if (element.removeClass) {
element.removeClass(value);
return element;
}
if (value && typeof value === "string" || value === undefined) {
var classNames = (value || "").split(/\s+/);
if (element.nodeType === 1 && element.className) {
if (value) {
var className = (" " + element.className + " ").replace(/[\n\t]/g, " ");
for (var c = 0, cl = classNames.length; c < cl; c++) {
className = className.replace(" " + classNames[c] + " ", " ");
}
element.className = className.replace(/^\s+|\s+$/g, "");
} else {
element.className = "";
}
}
}
return element;
};
var _getZoomFactor = function() {
var rect, physicalWidth, logicalWidth, zoomFactor = 1;
if (typeof document.body.getBoundingClientRect === "function") {
rect = document.body.getBoundingClientRect();
physicalWidth = rect.right - rect.left;
logicalWidth = document.body.offsetWidth;
zoomFactor = Math.round(physicalWidth / logicalWidth * 100) / 100;
}
return zoomFactor;
};
var _getDOMObjectPosition = function(obj) {
var info = {
left: 0,
top: 0,
width: 0,
height: 0,
zIndex: 999999999
};
var zi = _getStyle(obj, "z-index");
if (zi && zi !== "auto") {
info.zIndex = parseInt(zi, 10);
}
if (obj.getBoundingClientRect) {
var rect = obj.getBoundingClientRect();
var pageXOffset, pageYOffset, zoomFactor;
if ("pageXOffset" in window && "pageYOffset" in window) {
pageXOffset = window.pageXOffset;
pageYOffset = window.pageYOffset;
} else {
zoomFactor = _getZoomFactor();
pageXOffset = Math.round(document.documentElement.scrollLeft / zoomFactor);
pageYOffset = Math.round(document.documentElement.scrollTop / zoomFactor);
}
var leftBorderWidth = document.documentElement.clientLeft || 0;
var topBorderWidth = document.documentElement.clientTop || 0;
info.left = rect.left + pageXOffset - leftBorderWidth;
info.top = rect.top + pageYOffset - topBorderWidth;
info.width = "width" in rect ? rect.width : rect.right - rect.left;
info.height = "height" in rect ? rect.height : rect.bottom - rect.top;
}
return info;
};
var _noCache = function(path, options) {
var useNoCache = !(options && options.useNoCache === false);
if (useNoCache) {
return (path.indexOf("?") === -1 ? "?" : "&") + "nocache=" + new Date().getTime();
} else {
return "";
}
};
var _vars = function(options) {
var str = [];
var origins = [];
if (options.trustedOrigins) {
if (typeof options.trustedOrigins === "string") {
origins.push(options.trustedOrigins);
} else if (typeof options.trustedOrigins === "object" && "length" in options.trustedOrigins) {
origins = origins.concat(options.trustedOrigins);
}
}
if (options.trustedDomains) {
if (typeof options.trustedDomains === "string") {
origins.push(options.trustedDomains);
} else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) {
origins = origins.concat(options.trustedDomains);
}
}
if (origins.length) {
str.push("trustedOrigins=" + encodeURIComponent(origins.join(",")));
}
if (typeof options.amdModuleId === "string" && options.amdModuleId) {
str.push("amdModuleId=" + encodeURIComponent(options.amdModuleId));
}
if (typeof options.cjsModuleId === "string" && options.cjsModuleId) {
str.push("cjsModuleId=" + encodeURIComponent(options.cjsModuleId));
}
return str.join("&");
};
var _inArray = function(elem, array) {
if (array.indexOf) {
return array.indexOf(elem);
}
for (var i = 0, length = array.length; i < length; i++) {
if (array[i] === elem) {
return i;
}
}
return -1;
};
var _prepGlue = function(elements) {
if (typeof elements === "string") throw new TypeError("ZeroClipboard doesn't accept query strings.");
if (!elements.length) return [ elements ];
return elements;
};
var _dispatchCallback = function(func, element, instance, args, async) {
if (async) {
window.setTimeout(function() {
func.call(element, instance, args);
}, 0);
} else {
func.call(element, instance, args);
}
};
var currentElement, gluedElements = [], flashState = {};
var ZeroClipboard = function(elements, options) {
if (elements) (ZeroClipboard.prototype._singleton || this).glue(elements);
if (ZeroClipboard.prototype._singleton) return ZeroClipboard.prototype._singleton;
ZeroClipboard.prototype._singleton = this;
this.options = {};
for (var kd in _defaults) this.options[kd] = _defaults[kd];
for (var ko in options) this.options[ko] = options[ko];
this.handlers = {};
if (!flashState.hasOwnProperty(this.options.moviePath)) {
flashState[this.options.moviePath] = {
noflash: !ZeroClipboard.detectFlashSupport(),
wrongflash: false,
ready: false,
version: "0.0.0"
};
}
if (flashState[this.options.moviePath].noflash === false) {
_bridge();
}
};
ZeroClipboard.prototype.setCurrent = function(element) {
currentElement = element;
this.reposition();
var titleAttr = element.getAttribute("title");
if (titleAttr) {
this.setTitle(titleAttr);
}
var useHandCursor = this.options.forceHandCursor === true || _getStyle(element, "cursor") === "pointer";
_setHandCursor.call(this, useHandCursor);
return this;
};
ZeroClipboard.prototype.setText = function(newText) {
if (newText && newText !== "") {
this.options.text = newText;
if (this.ready()) this.flashBridge.setText(newText);
}
return this;
};
ZeroClipboard.prototype.setTitle = function(newTitle) {
if (newTitle && newTitle !== "") this.htmlBridge.setAttribute("title", newTitle);
return this;
};
ZeroClipboard.prototype.setSize = function(width, height) {
if (this.ready()) this.flashBridge.setSize(width, height);
return this;
};
ZeroClipboard.prototype.setHandCursor = function(enabled) {
enabled = typeof enabled === "boolean" ? enabled : !!enabled;
_setHandCursor.call(this, enabled);
this.options.forceHandCursor = enabled;
return this;
};
var _setHandCursor = function(enabled) {
if (this.ready()) this.flashBridge.setHandCursor(enabled);
};
ZeroClipboard.version = "1.2.3";
var _defaults = {
moviePath: "ZeroClipboard.swf",
trustedOrigins: null,
text: null,
hoverClass: "zeroclipboard-is-hover",
activeClass: "zeroclipboard-is-active",
allowScriptAccess: "sameDomain",
useNoCache: true,
forceHandCursor: false
};
ZeroClipboard.setDefaults = function(options) {
for (var ko in options) _defaults[ko] = options[ko];
};
ZeroClipboard.destroy = function() {
if (ZeroClipboard.prototype._singleton) {
ZeroClipboard.prototype._singleton.unglue(gluedElements);
var bridge = ZeroClipboard.prototype._singleton.htmlBridge;
if (bridge && bridge.parentNode) {
bridge.parentNode.removeChild(bridge);
}
delete ZeroClipboard.prototype._singleton;
}
};
ZeroClipboard.detectFlashSupport = function() {
var hasFlash = false;
if (typeof ActiveXObject === "function") {
try {
if (new ActiveXObject("ShockwaveFlash.ShockwaveFlash")) {
hasFlash = true;
}
} catch (error) {}
}
if (!hasFlash && navigator.mimeTypes["application/x-shockwave-flash"]) {
hasFlash = true;
}
return hasFlash;
};
var _amdModuleId = null;
var _cjsModuleId = null;
var _bridge = function() {
var flashBridge, len;
var client = ZeroClipboard.prototype._singleton;
var container = document.getElementById("global-zeroclipboard-html-bridge");
if (!container) {
var opts = {};
for (var ko in client.options) opts[ko] = client.options[ko];
opts.amdModuleId = _amdModuleId;
opts.cjsModuleId = _cjsModuleId;
var flashvars = _vars(opts);
var html = ' <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" id="global-zeroclipboard-flash-bridge" width="100%" height="100%"> <param name="movie" value="' + client.options.moviePath + _noCache(client.options.moviePath, client.options) + '"/> <param name="allowScriptAccess" value="' + client.options.allowScriptAccess + '"/> <param name="scale" value="exactfit"/> <param name="loop" value="false"/> <param name="menu" value="false"/> <param name="quality" value="best" /> <param name="bgcolor" value="#ffffff"/> <param name="wmode" value="transparent"/> <param name="flashvars" value="' + flashvars + '"/> <embed src="' + client.options.moviePath + _noCache(client.options.moviePath, client.options) + '" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="100%" height="100%" name="global-zeroclipboard-flash-bridge" allowScriptAccess="always" allowFullScreen="false" type="application/x-shockwave-flash" wmode="transparent" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="' + flashvars + '" scale="exactfit"> </embed> </object>';
container = document.createElement("div");
container.id = "global-zeroclipboard-html-bridge";
container.setAttribute("class", "global-zeroclipboard-container");
container.style.position = "absolute";
container.style.left = "0px";
container.style.top = "-9999px";
container.style.width = "15px";
container.style.height = "15px";
container.style.zIndex = "9999";
document.body.appendChild(container);
container.innerHTML = html;
}
client.htmlBridge = container;
flashBridge = document["global-zeroclipboard-flash-bridge"];
if (flashBridge && (len = flashBridge.length)) {
flashBridge = flashBridge[len - 1];
}
client.flashBridge = flashBridge || container.children[0].lastElementChild;
};
ZeroClipboard.prototype.resetBridge = function() {
if (this.htmlBridge) {
this.htmlBridge.style.left = "0px";
this.htmlBridge.style.top = "-9999px";
this.htmlBridge.removeAttribute("title");
}
if (currentElement) {
_removeClass(currentElement, this.options.activeClass);
currentElement = null;
}
this.options.text = null;
return this;
};
ZeroClipboard.prototype.ready = function() {
return flashState[this.options.moviePath].ready === true;
};
ZeroClipboard.prototype.reposition = function() {
if (!currentElement) return false;
var pos = _getDOMObjectPosition(currentElement);
this.htmlBridge.style.top = pos.top + "px";
this.htmlBridge.style.left = pos.left + "px";
this.htmlBridge.style.width = pos.width + "px";
this.htmlBridge.style.height = pos.height + "px";
this.htmlBridge.style.zIndex = pos.zIndex + 1;
this.setSize(pos.width, pos.height);
return this;
};
ZeroClipboard.dispatch = function(eventName, args) {
ZeroClipboard.prototype._singleton.receiveEvent(eventName, args);
};
ZeroClipboard.prototype.on = function(eventName, func) {
var events = eventName.toString().split(/\s/g), added = {};
for (var i = 0, len = events.length; i < len; i++) {
eventName = events[i].toLowerCase().replace(/^on/, "");
added[eventName] = true;
if (!this.handlers[eventName]) {
this.handlers[eventName] = func;
}
}
if (added.noflash && flashState[this.options.moviePath].noflash) {
this.receiveEvent("onNoFlash", {});
}
if (added.wrongflash && flashState[this.options.moviePath].wrongflash) {
this.receiveEvent("onWrongFlash", {
flashVersion: flashState[this.options.moviePath].version
});
}
if (added.load && flashState[this.options.moviePath].ready) {
this.receiveEvent("onLoad", {
flashVersion: flashState[this.options.moviePath].version
});
}
return this;
};
ZeroClipboard.prototype.addEventListener = ZeroClipboard.prototype.on;
ZeroClipboard.prototype.off = function(eventName, func) {
var events = eventName.toString().split(/\s/g);
for (var i = 0; i < events.length; i++) {
eventName = events[i].toLowerCase().replace(/^on/, "");
for (var event in this.handlers) {
if (event === eventName && this.handlers[event] === func) {
delete this.handlers[event];
}
}
}
return this;
};
ZeroClipboard.prototype.removeEventListener = ZeroClipboard.prototype.off;
ZeroClipboard.prototype.receiveEvent = function(eventName, args) {
eventName = eventName.toString().toLowerCase().replace(/^on/, "");
var element = currentElement;
var performCallbackAsync = true;
switch (eventName) {
case "load":
if (args && args.flashVersion) {
if (!_isFlashVersionSupported(args.flashVersion)) {
this.receiveEvent("onWrongFlash", {
flashVersion: args.flashVersion
});
return;
}
flashState[this.options.moviePath].ready = true;
flashState[this.options.moviePath].version = args.flashVersion;
}
break;
case "wrongflash":
if (args && args.flashVersion && !_isFlashVersionSupported(args.flashVersion)) {
flashState[this.options.moviePath].wrongflash = true;
flashState[this.options.moviePath].version = args.flashVersion;
}
break;
case "mouseover":
_addClass(element, this.options.hoverClass);
break;
case "mouseout":
_removeClass(element, this.options.hoverClass);
this.resetBridge();
break;
case "mousedown":
_addClass(element, this.options.activeClass);
break;
case "mouseup":
_removeClass(element, this.options.activeClass);
break;
case "datarequested":
var targetId = element.getAttribute("data-clipboard-target"), targetEl = !targetId ? null : document.getElementById(targetId);
if (targetEl) {
var textContent = targetEl.value || targetEl.textContent || targetEl.innerText;
if (textContent) {
this.setText(textContent);
}
} else {
var defaultText = element.getAttribute("data-clipboard-text");
if (defaultText) {
this.setText(defaultText);
}
}
performCallbackAsync = false;
break;
case "complete":
this.options.text = null;
break;
}
if (this.handlers[eventName]) {
var func = this.handlers[eventName];
if (typeof func === "string" && typeof window[func] === "function") {
func = window[func];
}
if (typeof func === "function") {
_dispatchCallback(func, element, this, args, performCallbackAsync);
}
}
};
ZeroClipboard.prototype.glue = function(elements) {
elements = _prepGlue(elements);
for (var i = 0; i < elements.length; i++) {
if (elements[i] && elements[i].nodeType === 1) {
if (_inArray(elements[i], gluedElements) == -1) {
gluedElements.push(elements[i]);
_addEventHandler(elements[i], "mouseover", _elementMouseOver);
}
}
}
return this;
};
ZeroClipboard.prototype.unglue = function(elements) {
elements = _prepGlue(elements);
for (var i = 0; i < elements.length; i++) {
_removeEventHandler(elements[i], "mouseover", _elementMouseOver);
var arrayIndex = _inArray(elements[i], gluedElements);
if (arrayIndex != -1) gluedElements.splice(arrayIndex, 1);
}
return this;
};
function _isFlashVersionSupported(flashVersion) {
return parseFloat(flashVersion.replace(/,/g, ".").replace(/[^0-9\.]/g, "")) >= 10;
}
if (typeof define === "function" && define.amd) {
define([ "require", "exports", "module" ], function(require, exports, module) {
_amdModuleId = module && module.id || null;
return ZeroClipboard;
});
} else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) {
_cjsModuleId = module.id || null;
module.exports = ZeroClipboard;
} else {
window.ZeroClipboard = ZeroClipboard;
}
})();