Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

92 allow donate amount of choosing #215

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,5 @@ gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
# Explicitly depend on and compile nokogiri
# so we can run CI on Ruby head
gem 'nokogiri', '~> 1.14', force_ruby_platform: true

gem 'stripe'
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ GEM
sprockets (>= 3.0.0)
stimulus-rails (1.2.1)
railties (>= 6.0.0)
stripe (10.6.0)
thor (1.2.2)
tilt (2.2.0)
timeout (0.4.0)
Expand Down Expand Up @@ -436,6 +437,7 @@ DEPENDENCIES
shoulda-matchers (~> 5.3.0)
slack-ruby-client (~> 2.0.0)
stimulus-rails (~> 1.2.1)
stripe
truncate_html (~> 0.9.3)
turbo-rails (~> 1.4.0)
tzinfo-data
Expand Down
41 changes: 41 additions & 0 deletions app/controllers/api/donations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Api
class DonationsController < ApplicationController
def create
Stripe.api_key = stripe_key

session = Stripe::Checkout::Session.create({
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: 'Donation',
},
unit_amount: price_in_cents,
},
quantity: 1,
}],
mode: 'payment',
# required by stripe
success_url: 'https://wnb-rb.dev',
cancel_url: 'https://wnb-rb.dev/donate',
})
render json: { url: session.url }
end

private

def stripe_key
ENV.fetch('STRIPE_KEY', nil)
end

def price_in_cents
donation_params[:price].to_i * 100
end

def donation_params
params.require(:donation).permit(:price)
end
end
end
109 changes: 87 additions & 22 deletions app/javascript/components/pages/Donate.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useState, useMemo } from 'react';
import { Helmet } from 'react-helmet-async';
import propTypes from 'prop-types';
import SharedLayout from 'components/layout/SharedLayout';
import Banner from 'components/Banner';
import Button from 'components/Button';
import PageTitleWithContainer from 'components/PageTitleWithContainer';
import { donationAmounts } from 'datasources';
Expand Down Expand Up @@ -49,7 +48,6 @@ const Donate = () => {
</ul>
</div>
</div>
<OtherAmountBanner />
</SharedLayout>
</>
);
Expand All @@ -64,6 +62,61 @@ const DonationAmounts = ({ selectedPrice, setSelectedPrice }) => {
})[0];
}, [prices, selectedPrice]);

const [customSelected, setCustomSelected] = useState(false);
const [customAmount, setCustomAmount] = useState('');

const inputHandler = (e) => {
const input = e.target.value;
setCustomAmount(input);
};

const donateOtherHandler = () => {
if (!isValidNumber(customAmount)) return;

const newWindow = window.open('', '_blank');
const body = { price: customAmount };

const fetchCheckoutUrl = async () => {
const result = await fetch('/api/donations', {
method: 'POST',
headers: {
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (result.status === 200) {
const json = await result.json();
setCustomSelected(false);
setCustomAmount('');
newWindow.location = json.url;
}
};

fetchCheckoutUrl();
};

const selectSetPrice = (value) => {
setSelectedPrice(value);
setCustomSelected(false);
};

const isWholeNumber = (value) => {
return /^\d{1,}$/.test(value);
};

const isValidNumber = (value) => {
return isWholeNumber(value) && parseInt(customAmount, 10) > 0;
};

const customAmtButtonText = () => {
return isValidNumber(customAmount)
? `Donate $${parseInt(customAmount, 10).toLocaleString()}`
: customAmount === '' || customAmount === '0'
? 'Donate'
: 'Please enter a dollar amount';
};

return (
<div className="flex justify-center">
<div className="donation-amounts">
Expand All @@ -72,37 +125,49 @@ const DonationAmounts = ({ selectedPrice, setSelectedPrice }) => {
<button
key={price.value}
className={`donation-amount ${
selectedPrice === price.value ? 'selected' : null
selectedPrice === price.value && !customSelected ? 'selected' : null
}`}
onClick={() => setSelectedPrice(price.value)}
onClick={() => selectSetPrice(price.value)}
>
${price.value.toLocaleString()}
</button>
);
})}
<Button type="secondary" className="donate-button">
<a href={selectedPriceObject.link} target="_blank" rel="noopener noreferrer">
Donate ${selectedPriceObject.value.toLocaleString()}
</a>
</Button>
<form
className={`donation-amount flex-col p-1 ${customSelected ? 'selected' : null}`}
>
<label htmlFor="other">Other</label>
<input
id="other"
type="text"
className="max-w-full m-0 p-1"
onClick={() => setCustomSelected(true)}
onKeyDown={() => setCustomSelected(true)}
value={customAmount}
onChange={inputHandler}
/>
</form>
{!customSelected && (
<Button type="secondary" className="donate-button">
<a
href={selectedPriceObject.link}
target="_blank"
rel="noopener noreferrer"
>
Donate ${selectedPriceObject.value.toLocaleString()}
</a>
</Button>
)}
{customSelected && (
<button className="button secondary donate-button" onClick={donateOtherHandler}>
{customAmtButtonText()}
</button>
)}
</div>
</div>
);
};

const OtherAmountBanner = () => {
return (
<Banner>
Interested in donating a different amount?
<Button type="white" className="ml-0 md:ml-5 mt-5 md:mt-0">
<a href={'mailto:organizers@wnb-rb.dev'} target="_blank" rel="noopener noreferrer">
Contact Us
</a>
</Button>
</Banner>
);
};

DonationAmounts.propTypes = {
selectedPrice: propTypes.number,
setSelectedPrice: propTypes.func,
Expand Down
2 changes: 0 additions & 2 deletions app/javascript/datasources/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const donationAmounts = (environment) => {
};

const testDonationAmounts = [
{ value: 10, link: 'https://buy.stripe.com/test_fZe3cr0hWfIN0i4289' },
{ value: 25, link: 'https://buy.stripe.com/test_14k3crfcQaot7Kw6oq' },
{ value: 50, link: 'https://buy.stripe.com/test_6oEaET9Sw1RXfcY8wz' },
{ value: 75, link: 'https://buy.stripe.com/test_8wM28n4yc7chfcY007' },
Expand All @@ -88,7 +87,6 @@ const testDonationAmounts = [
];

const productionDonationAmounts = [
{ value: 10, link: 'https://buy.stripe.com/14k14KcpbcJ4bwQbIK' },
{ value: 25, link: 'https://buy.stripe.com/dR600G74RbF0fN6003' },
{ value: 50, link: 'https://buy.stripe.com/7sI00G60N9wSeJ2bIM' },
{ value: 75, link: 'https://buy.stripe.com/6oEeVA60NbF06cwcMR' },
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
end

post 'register-user', to: 'registrations#register_user'

resources :donations, only: [:create]
end

mount ActionCable.server => '/cable'
Expand Down
12 changes: 12 additions & 0 deletions spec/controllers/api/donations_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

require './spec/rails_helper'

RSpec.describe Api::DonationsController, type: :controller do
describe 'POST #create' do
it 'calls create on a stripe checkout session' do
expect(Stripe::Checkout::Session).to receive(:create).and_return(double(url: 'http://example.com'))
post :create, params: { donation: { price: 10 } }
end
end
end
41 changes: 41 additions & 0 deletions spec/system/custom_donation_system_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Custom donation system', type: :system, js: true do
before do
visit donate_path
end

describe 'user inputs custom donation amount' do
context 'user inputs an integer' do
it 'updates donate button text entered amount' do
find('#other').click
fill_in('Other', with: '40')
expect(page).to have_button('Donate $40')
end

it 'does not update button text if amount is 0' do
find('#other').click
fill_in('Other', with: '0')
expect(page).to have_button('Donate')
end

it 'opens a new window when donate button is clicked' do
allow(Stripe::Checkout::Session).to receive(:create).and_return(double(url: ''))
find('#other').click
fill_in('Other', with: '40')
click_on 'Donate $40'
expect(page.driver.browser.window_handles.size).to eq 2
end
end

context 'user inputs non-integer' do
it 'updates donate button to ask for a dollar amount' do
find('#other').click
fill_in('Other', with: 'not number')
expect(page).to have_button('Please enter a dollar amount')
end
end
end
end