├── .gitignore ├── CHANGELOG.markdown ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.markdown ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── powered-by-stripe.png │ │ └── processing.gif │ ├── javascripts │ │ ├── app │ │ │ ├── connect.coffee │ │ │ ├── niceties.coffee │ │ │ ├── pay.coffee │ │ │ └── subscribe.coffee │ │ └── application.js │ └── stylesheets │ │ └── application.scss ├── controllers │ ├── application_controller.rb │ ├── home_controller.rb │ ├── hooks_controller.rb │ ├── sessions_controller.rb │ ├── stripe_controller.rb │ └── users_controller.rb ├── helpers │ └── application_helper.rb ├── models │ └── user.rb ├── services │ ├── stripe_managed.rb │ ├── stripe_oauth.rb │ └── stripe_standalone.rb └── views │ ├── home │ └── index.html.haml │ ├── layouts │ └── application.html.haml │ ├── sessions │ └── new.html.haml │ └── users │ ├── _connect.html.haml │ ├── _nav.html.haml │ ├── _pay.html.haml │ ├── _settings.html.haml │ ├── index.html.haml │ ├── new.html.haml │ └── show.html.haml ├── bin ├── bundle ├── rails └── rake ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── rest_client.rb │ ├── session_store.rb │ ├── stripe.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── routes.rb └── secrets.sample.yml ├── db ├── migrate │ ├── 20141130003114_create_users.rb │ ├── 20150310212328_add_stripe_account_type_to_users.rb │ └── 20150318194136_add_managed_account_status_to_users.rb ├── schema.rb └── seeds.rb ├── docs ├── api-keys.png ├── app-setup.png └── development-mode-bar.png ├── lib └── tasks │ └── setup.rake └── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /db/*.sqlite3 3 | /db/*.sqlite3-journal 4 | /log/*.log 5 | /tmp 6 | config/secrets.yml 7 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | ## June 8, 2015 ## 2 | 3 | * The country code parameter for UK accounts should be `GB` (not `UK`). 4 | * Upgrade to API version `2015-04-07`, no changes required. 5 | * Update bindings to 1.21.0 6 | * Fix 'verification must be a hash' and other issues with updating 7 | managed accounts for verification by handling the `legal_entity` 8 | update differently. Since 1.20.2 updating nested hashes works, so 9 | we now do that instead of building a new `legal_entity` from scratch. 10 | 11 | 12 | ## February 24, 2015 ## 13 | 14 | * Update code to latest API version (`2015-02-18`), specify in 15 | initializer to use this version, and note the version in README. 16 | * Improve/tweak the setup script. 17 | * Upgrade to Rails 4.2. 18 | 19 | 20 | ## December 11, 2014 ## 21 | 22 | * Shortest `client_id` is now 17 chars (`ca_**************`) 23 | 24 | 25 | ## December 6, 2014 ## 26 | 27 | * Changed the webhook handling code quite a bit to make it 28 | cleaner and simpler. Specifically the case where the account 29 | was deauthorized - the handler now handles 401 responses 30 | from the API rather than just sort of `rescue nil` and assuming. 31 | * Mentioned needing `bundler` in addition to Ruby. 32 | 33 | 34 | ## December 2, 2014 ## 35 | 36 | * Initial release. 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '4.2' 4 | gem 'sqlite3' 5 | gem 'thin' 6 | 7 | gem 'stripe' 8 | gem 'oauth2' 9 | 10 | gem 'haml' 11 | gem 'coffee-rails' 12 | gem 'sass-rails', '~> 4.0.3' 13 | gem 'jquery-rails' 14 | gem 'bootstrap-sass', '~> 3.3.1' 15 | 16 | gem 'bcrypt', '~> 3.1.7' 17 | 18 | gem 'quiet_assets' 19 | 20 | gem 'highline', require: false # used for setup rake task 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.0) 5 | actionpack (= 4.2.0) 6 | actionview (= 4.2.0) 7 | activejob (= 4.2.0) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.0) 11 | actionview (= 4.2.0) 12 | activesupport (= 4.2.0) 13 | rack (~> 1.6.0) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.0) 18 | activesupport (= 4.2.0) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | activejob (4.2.0) 24 | activesupport (= 4.2.0) 25 | globalid (>= 0.3.0) 26 | activemodel (4.2.0) 27 | activesupport (= 4.2.0) 28 | builder (~> 3.1) 29 | activerecord (4.2.0) 30 | activemodel (= 4.2.0) 31 | activesupport (= 4.2.0) 32 | arel (~> 6.0) 33 | activesupport (4.2.0) 34 | i18n (~> 0.7) 35 | json (~> 1.7, >= 1.7.7) 36 | minitest (~> 5.1) 37 | thread_safe (~> 0.3, >= 0.3.4) 38 | tzinfo (~> 1.1) 39 | arel (6.0.0) 40 | bcrypt (3.1.9) 41 | bootstrap-sass (3.3.1.0) 42 | sass (~> 3.2) 43 | builder (3.2.2) 44 | coffee-rails (4.0.1) 45 | coffee-script (>= 2.2.0) 46 | railties (>= 4.0.0, < 5.0) 47 | coffee-script (2.3.0) 48 | coffee-script-source 49 | execjs 50 | coffee-script-source (1.8.0) 51 | daemons (1.1.9) 52 | domain_name (0.5.24) 53 | unf (>= 0.0.5, < 1.0.0) 54 | erubis (2.7.0) 55 | eventmachine (1.0.3) 56 | execjs (2.2.2) 57 | faraday (0.9.0) 58 | multipart-post (>= 1.2, < 3) 59 | globalid (0.3.3) 60 | activesupport (>= 4.1.0) 61 | haml (4.0.5) 62 | tilt 63 | highline (1.6.21) 64 | hike (1.2.3) 65 | http-cookie (1.0.2) 66 | domain_name (~> 0.5) 67 | i18n (0.7.0) 68 | jquery-rails (3.1.2) 69 | railties (>= 3.0, < 5.0) 70 | thor (>= 0.14, < 2.0) 71 | json (1.8.3) 72 | jwt (1.0.0) 73 | loofah (2.0.1) 74 | nokogiri (>= 1.5.9) 75 | mail (2.6.3) 76 | mime-types (>= 1.16, < 3) 77 | mime-types (2.6.1) 78 | mini_portile (0.6.2) 79 | minitest (5.5.1) 80 | multi_json (1.10.1) 81 | multi_xml (0.5.5) 82 | multipart-post (2.0.0) 83 | netrc (0.10.3) 84 | nokogiri (1.6.6.2) 85 | mini_portile (~> 0.6.0) 86 | oauth2 (1.0.0) 87 | faraday (>= 0.8, < 0.10) 88 | jwt (~> 1.0) 89 | multi_json (~> 1.3) 90 | multi_xml (~> 0.5) 91 | rack (~> 1.2) 92 | quiet_assets (1.0.3) 93 | railties (>= 3.1, < 5.0) 94 | rack (1.6.0) 95 | rack-test (0.6.3) 96 | rack (>= 1.0) 97 | rails (4.2.0) 98 | actionmailer (= 4.2.0) 99 | actionpack (= 4.2.0) 100 | actionview (= 4.2.0) 101 | activejob (= 4.2.0) 102 | activemodel (= 4.2.0) 103 | activerecord (= 4.2.0) 104 | activesupport (= 4.2.0) 105 | bundler (>= 1.3.0, < 2.0) 106 | railties (= 4.2.0) 107 | sprockets-rails 108 | rails-deprecated_sanitizer (1.0.3) 109 | activesupport (>= 4.2.0.alpha) 110 | rails-dom-testing (1.0.5) 111 | activesupport (>= 4.2.0.beta, < 5.0) 112 | nokogiri (~> 1.6.0) 113 | rails-deprecated_sanitizer (>= 1.0.1) 114 | rails-html-sanitizer (1.0.1) 115 | loofah (~> 2.0) 116 | railties (4.2.0) 117 | actionpack (= 4.2.0) 118 | activesupport (= 4.2.0) 119 | rake (>= 0.8.7) 120 | thor (>= 0.18.1, < 2.0) 121 | rake (10.4.2) 122 | rest-client (1.8.0) 123 | http-cookie (>= 1.0.2, < 2.0) 124 | mime-types (>= 1.16, < 3.0) 125 | netrc (~> 0.7) 126 | sass (3.2.19) 127 | sass-rails (4.0.5) 128 | railties (>= 4.0.0, < 5.0) 129 | sass (~> 3.2.2) 130 | sprockets (~> 2.8, < 3.0) 131 | sprockets-rails (~> 2.0) 132 | sprockets (2.12.3) 133 | hike (~> 1.2) 134 | multi_json (~> 1.0) 135 | rack (~> 1.0) 136 | tilt (~> 1.1, != 1.3.0) 137 | sprockets-rails (2.2.4) 138 | actionpack (>= 3.0) 139 | activesupport (>= 3.0) 140 | sprockets (>= 2.8, < 4.0) 141 | sqlite3 (1.3.10) 142 | stripe (1.21.0) 143 | json (~> 1.8.1) 144 | rest-client (~> 1.4) 145 | thin (1.6.2) 146 | daemons (>= 1.0.9) 147 | eventmachine (>= 1.0.0) 148 | rack (>= 1.0.0) 149 | thor (0.19.1) 150 | thread_safe (0.3.4) 151 | tilt (1.4.1) 152 | tzinfo (1.2.2) 153 | thread_safe (~> 0.1) 154 | unf (0.1.4) 155 | unf_ext 156 | unf_ext (0.0.7.1) 157 | 158 | PLATFORMS 159 | ruby 160 | 161 | DEPENDENCIES 162 | bcrypt (~> 3.1.7) 163 | bootstrap-sass (~> 3.3.1) 164 | coffee-rails 165 | haml 166 | highline 167 | jquery-rails 168 | oauth2 169 | quiet_assets 170 | rails (= 4.2) 171 | sass-rails (~> 4.0.3) 172 | sqlite3 173 | stripe 174 | thin 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Ryan Funduk (http://ryanfunduk.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Rails + Stripe Connect Example Application 2 | 3 | This repository contains a bare-bones example Stripe Connect application. 4 | It's purpose is to demonstrate the various pieces needed to get up and 5 | running with your own application. 6 | 7 | **This application is not meant to be used as a starting point or run in production**! 8 | 9 | ## Pre-requisites 10 | 11 | This application currently uses the following Stripe API version: [**2015-04-07**](https://stripe.com/docs/upgrades#2015-04-07) 12 | 13 | This application is based on **Rails 4.2** and running on **Ruby 2.1** (with 14 | [`bundler`](http://bundler.io/)). It will probably work fine with versions after 15 | these, and I will endeavour to keep it up to date. 16 | 17 | You need a Stripe account configured with a Connect application. 18 | That can be setup in your 19 | [Account Settings under 'Apps'](https://dashboard.stripe.com/account/applications/settings). 20 | Here's what it should look like configured: 21 | 22 | ![App Configuration](./docs/app-setup.png) 23 | 24 | Note the 'Redirect URIs' setting. 25 | 26 | You'll also need the regular API keys for the same account, which 27 | you can also get in [Account Settings under 'API Keys'](https://dashboard.stripe.com/account/apikeys). 28 | 29 | ![API Keys](./docs/api-keys.png) 30 | 31 | 32 | ## Setup 33 | 34 | To get started, first clone this repo and install dependencies: 35 | 36 | git clone git@github.com:rfunduk/rails-stripe-connect-example.git 37 | cd rails-stripe-connect-example 38 | bundle install 39 | 40 | Next we need to run the setup script that will put your various Stripe 41 | credentials in the appropriate place. Since this will be asking for API 42 | keys, you probably want to [read it over](./lib/tasks/setup.rake) first 43 | to be confident nothing nefarious is being done with your API keys :) 44 | 45 | bin/rake app:setup 46 | 47 | Once you get through that, your keys will be in `config/secrets.yml` and 48 | picked up by Rails when you start it. 49 | 50 | Now load the schema into the database: 51 | 52 | bin/rake db:schema:load 53 | 54 | And start up the server: 55 | 56 | bin/rails s 57 | 58 | Then as usual visit [http://localhost:3000](http://localhost:3000) in your 59 | browser of choice. 60 | 61 | ### Webhooks 62 | 63 | Optionally, if you want webhooks to work, signup for [ngrok](https://ngrok.com) 64 | (and donate!). Then run: 65 | 66 | ngrok -authtoken=NGROK_TOKEN -subdomain=a-name 3000 67 | 68 | Then configure your Stripe Connect application's 'Webhook URL' on the 69 | [application settings page](https://dashboard.stripe.com/account/applications/settings) 70 | to be `http://a-name.ngrok.com/hooks/stripe`. 71 | 72 | The `account.application.deauthorized` webhook will do the right 73 | things, but to see others you'll want to just look at the Rails request log. 74 | 75 | ## How It Works 76 | 77 | ### Step 0 78 | 79 | If you want to try out a subscription with Stripe Connect, 80 | [add some plans](https://dashboard.stripe.com/test/plans) 81 | to your application's account on the dashboard. 82 | 83 | ### Step 1 84 | 85 | Visit the app and click 'Get Started'. You'll be prompted to 86 | create a user account. This app has a basic user login/cookie-based 87 | session system. 88 | 89 | ### Step 2 90 | 91 | Connect to Stripe. You'll have the option of 3 different types 92 | of connection: 93 | 94 | ##### 1. OAuth Standalone 95 | 96 | Create an account or connect to an existing account via an OAuth flow. 97 | 98 | You may want to do this in an incognito window or similar so that you don't 99 | accidentally connect your platform/main account to itself which will be 100 | very confusing. 101 | 102 | It's probably best to make another Stripe account with a test email 103 | address (eg, with Gmail you can do things like `you+stripetest1@gmail.com` 104 | to make this easier), or you can just use the 'Create New Account...' 105 | option in the menu at the top right of your Stripe dashboard. 106 | 107 | When you click 'Connect', look for the development mode bar 108 | at the top of the page: 109 | 110 | ![Development Mode Prompt](./docs/development-mode-bar.png) 111 | 112 | ...and click _Skip this account form..._ if you aren't activated yet. 113 | 114 | This account + Stripe connection becomes the 'seller'. 115 | 116 | ##### 2. Standalone Account via API 117 | 118 | You can create a standalone Stripe account via the API, which doesn't require 119 | the user to leave your site at all. 120 | 121 | Doing this is a simple matter of choosing a country and clicking 'Create' 122 | 123 | This account + Stripe connection becomes the 'seller'. 124 | 125 | ##### 3. Managed Account via API 126 | 127 | You can create an entirely managed-by-you Stripe account via the API. 128 | With this method the user will have the least interaction with Stripe. 129 | 130 | Doing this is a simple matter of choosing a country, agreeing to the Stripe 131 | Terms of Service, and clicking 'Create'. 132 | 133 | Currently managed accounts are in beta and only available to US or Canadian 134 | platform accounts. 135 | 136 | This account + Stripe connection becomes the 'seller'. 137 | 138 | 139 | ### Step 3 140 | 141 | Now log out of the example app and signup again for another account. 142 | This time don't bother connecting to Stripe (although you can if you want). 143 | 144 | This account becomes the 'buyer'. 145 | 146 | ### Step 4 147 | 148 | Purchase something from the seller as the buyer! Visit 149 | [http://localhost:3000/users](http://localhost:3000/users) and 150 | choose the connected/seller account. And make a payment or 151 | subscribe to a plan. 152 | 153 | ### Step 5 154 | 155 | Go through the code! I've tried to heavily comment the relevant and most 156 | important parts of the code. Let me know if anything is unclear or 157 | broken by opening an issue or [sending me an email](http://ryanfunduk.com). 158 | 159 | I suggest perusing [the Connect docs](https://stripe.com/docs/connect) before 160 | trying to dig into the code. 161 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/images/powered-by-stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfunduk/rails-stripe-connect-example/651d85a7ab1da610d163d18003c93bb6078524e9/app/assets/images/powered-by-stripe.png -------------------------------------------------------------------------------- /app/assets/images/processing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfunduk/rails-stripe-connect-example/651d85a7ab1da610d163d18003c93bb6078524e9/app/assets/images/processing.gif -------------------------------------------------------------------------------- /app/assets/javascripts/app/connect.coffee: -------------------------------------------------------------------------------- 1 | $(document).ready -> 2 | # pre-connection 3 | setupManaged() 4 | setupStandalone() 5 | 6 | # connected user, but we need info 7 | setupFieldsNeeded() 8 | 9 | setupManaged = -> 10 | container = $('#stripe-managed') 11 | return if container.length == 0 12 | tosEl = container.find('.tos input') 13 | countrySelect = container.find('.country') 14 | form = container.find('form') 15 | createButton = form.find('.btn') 16 | 17 | tosEl.change -> createButton.toggleClass 'disabled', !tosEl.is(':checked') 18 | form.submit ( e ) -> 19 | # prevent creation unless ToS is checked 20 | if !tosEl.is(':checked') 21 | e.preventDefault() 22 | return false 23 | 24 | createButton.addClass('disabled').val('...') 25 | 26 | # populate appropriate ToS link depending on dropdown 27 | countrySelect.change -> 28 | termsUrl = "https://stripe.com/#{countrySelect.val().toLowerCase()}/terms" 29 | tosEl.siblings('a').attr( href: termsUrl ) 30 | 31 | setupStandalone = -> 32 | container = $('#stripe-standalone') 33 | return if container.length == 0 34 | countrySelect = container.find('.country') 35 | form = container.find('form') 36 | createButton = form.find('.btn') 37 | 38 | form.submit ( e ) -> 39 | createButton.addClass('disabled').val('...') 40 | 41 | setupFieldsNeeded = -> 42 | container = $('.needed') 43 | return if container.length == 0 44 | 45 | form = container.find('form') 46 | 47 | form.submit ( e ) -> 48 | button = form.find('.buttons .btn') 49 | button.addClass('disabled').val('Saving...') 50 | 51 | # bank account tokenization 52 | if (baContainer = form.find('#bank-account')).length > 0 53 | Stripe.setPublishableKey baContainer.data('publishable') 54 | tokenField = form.find('#bank_account_token') 55 | if tokenField.is(':empty') 56 | e.preventDefault() 57 | Stripe.bankAccount.createToken form, ( _, resp ) -> 58 | if resp.error 59 | button.removeClass('disabled').val('Save Info') 60 | alert( resp.error.message ) 61 | else 62 | tokenField.val( resp.id ) 63 | form.get(0).submit() 64 | return false 65 | -------------------------------------------------------------------------------- /app/assets/javascripts/app/niceties.coffee: -------------------------------------------------------------------------------- 1 | $(document).ready -> 2 | setTimeout( 3 | -> $('.alert.alert-info.auto').slideUp('fast') 4 | 3500 5 | ) 6 | 7 | $('body').on 'click', 'a[rel=platform-account]', ( e ) -> 8 | return confirm("This link will only work if you're logged in as the **application owner**. Continue?") 9 | $('body').on 'click', 'a[rel=connected-account]', ( e ) -> 10 | return confirm("This link will only work if you're logged in as the **connected account**. Continue?") 11 | -------------------------------------------------------------------------------- /app/assets/javascripts/app/pay.coffee: -------------------------------------------------------------------------------- 1 | $(document).ready -> 2 | return unless StripeCheckout? 3 | 4 | # Keeps track of whether we're in the middle of processing 5 | # a payment or not. This way we can tell if the 'closed' 6 | # event was due to a successful token generation, or the user 7 | # closing it by hand. 8 | submitting = false 9 | 10 | payButton = $('.pay-button') 11 | form = payButton.closest('form') 12 | destination = form.find('select[name=charge_on]') 13 | indicator = form.find('.indicator').height( form.outerHeight() ) 14 | handler = null 15 | 16 | createHandler = -> 17 | handler = StripeCheckout.configure 18 | # Grab the correct publishable key. Depending on 19 | # the selection in the interface. 20 | key: window.publishable[destination.val()] 21 | 22 | # The email of the logged in user. 23 | email: window.currentUserEmail 24 | 25 | allowRememberMe: false 26 | closed: -> 27 | form.removeClass('processing') unless submitting 28 | token: ( token ) -> 29 | submitting = true 30 | form.find('input[name=token]').val( token.id ) 31 | form.get(0).submit() 32 | 33 | destination.change createHandler 34 | createHandler() 35 | 36 | payButton.click ( e ) -> 37 | e.preventDefault() 38 | form.addClass( 'processing' ) 39 | 40 | handler.open 41 | name: 'Rails Connect Example' 42 | description: '$10 w/ 10% fees' 43 | amount: 1000 44 | -------------------------------------------------------------------------------- /app/assets/javascripts/app/subscribe.coffee: -------------------------------------------------------------------------------- 1 | $(document).ready -> 2 | return unless StripeCheckout? 3 | 4 | # Holds the plan selected by the user in the interface. 5 | currentPlan = null 6 | 7 | # Keeps track of whether we're in the middle of processing 8 | # a payment or not. This way we can tell if the 'closed' 9 | # event was due to a successful token generation, or the user 10 | # closing it by hand. 11 | submitting = false 12 | 13 | subscribeButton = $('.subscribe-button') 14 | planButtons = $('.plan-choice') 15 | form = subscribeButton.closest('form') 16 | destination = form.find('select[name=charge_on]') 17 | indicator = form.find('.indicator').height( form.outerHeight() ) 18 | 19 | handler = StripeCheckout.configure 20 | # The publishable key of the **connected account**. 21 | key: window.publishable[destination.val()] 22 | 23 | # The email of the logged in user. 24 | email: window.currentUserEmail 25 | 26 | allowRememberMe: false 27 | closed: -> 28 | subscribeButton.attr( disabled: true ) 29 | planButtons.removeClass('active') 30 | currentPlan = null 31 | form.removeClass('processing') unless submitting 32 | token: ( token ) -> 33 | submitting = true 34 | form.find('input[name=token]').val( token.id ) 35 | form.get(0).submit() 36 | 37 | planButtons.click ( e ) -> 38 | e.preventDefault() 39 | 40 | planButton = $(this) 41 | planButton.addClass('active').siblings().removeClass('active') 42 | subscribeButton.attr( disabled: false ) 43 | 44 | # Get current plan info from the clicked element's data attributes 45 | currentPlan = 46 | id: planButton.data('id') 47 | name: planButton.data('name') 48 | currency: planButton.data('currency') 49 | amount: parseInt planButton.data('amount'), 10 50 | 51 | form.find('input[name=plan]').val( currentPlan.id ) 52 | 53 | subscribeButton.show() 54 | 55 | subscribeButton.click ( e ) -> 56 | e.preventDefault() 57 | form.addClass('processing') 58 | 59 | if currentPlan == null 60 | alert "Choose a plan first!" 61 | return 62 | 63 | handler.open 64 | name: 'Rails Connect Example' 65 | description: "#{currentPlan.name} Subscription" 66 | amount: currentPlan.amount 67 | -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | //= require jquery 2 | //= require jquery_ujs 3 | //= require bootstrap-sprockets 4 | // 5 | //= require app/niceties 6 | //= require app/connect 7 | //= require app/pay 8 | //= require app/subscribe 9 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * These styles are not very interesting. 3 | * I could have easily gotten away with straight 4 | * Bootstrap with no customization at all... 5 | * 6 | * ...but I just can't do it :) 7 | */ 8 | 9 | $brand-primary: #00c38b; 10 | $brand-danger: #d9625a; 11 | $brand-info: #86ded1; 12 | $brand-warning: #f0c24a; 13 | $brand-success: #75b84c; 14 | 15 | $input-border-focus: #c3c3c3; 16 | 17 | @import 'bootstrap-sprockets'; 18 | @import 'bootstrap'; 19 | 20 | h1 { 21 | text-align: center; 22 | margin-top: 8px; 23 | margin-bottom: 2px; 24 | } 25 | h4 { 26 | text-align: center; 27 | margin-top: 0; 28 | margin-bottom: 20px; 29 | font-size: 16px; 30 | font-weight: normal; 31 | color: #666; 32 | } 33 | 34 | p:last-child { 35 | margin-bottom: 0; 36 | } 37 | 38 | .btn:focus { outline: none !important; } 39 | .btn-danger, .btn-primary, .btn-success { background: white; &:hover { background: white; } } 40 | .btn[disabled], .btn.disabled, 41 | .btn[disabled]:active, .btn.disabled:active, 42 | .btn[disabled]:focus, .btn.disabled:focus { background: white; border-color: #DDD; color: #AAA; } 43 | .btn-danger { color: $brand-danger; &:hover { color: darken( $brand-danger, 15% ); } } 44 | .btn-primary { color: $brand-primary; &:hover { color: darken( $brand-primary, 15% ); } } 45 | .btn-success { color: $brand-success; &:hover { color: darken( $brand-success, 15% ); } } 46 | 47 | .alert { 48 | background: white !important; 49 | } 50 | 51 | form { 52 | h2 { 53 | margin-top: 0; 54 | margin-bottom: 16px; 55 | text-align: center; 56 | } 57 | ul { 58 | padding-left: 16px; 59 | } 60 | a { 61 | cursor: pointer; 62 | } 63 | select { 64 | vertical-align: middle; 65 | } 66 | &.form-horizontal label.control-label { 67 | text-align: left; 68 | font-size: 12px; 69 | font-weight: normal; 70 | padding-top: 10px; 71 | } 72 | button.main-button { 73 | display: block; 74 | margin: 0 auto; 75 | padding: 8px 40px; 76 | } 77 | .indicator { 78 | display: none; 79 | width: 100%; 80 | background: white image-url('processing.gif') no-repeat center center; 81 | } 82 | &.processing { 83 | * { display: none; } 84 | .indicator { display: block !important; } 85 | } 86 | } 87 | 88 | .form-control:focus { 89 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 90 | 0 0 3px rgba(195, 195, 195, 0.6); 91 | } 92 | 93 | .panel { 94 | h2, h3 { 95 | margin-top: 0; 96 | } 97 | a.pull-right { 98 | margin-left: 16px; 99 | } 100 | } 101 | .list-group { 102 | margin-bottom: 10px; 103 | } 104 | .list-group:last-child { 105 | margin-bottom: 0; 106 | } 107 | .list-group-item { 108 | border-color: #EEE; 109 | h3 { 110 | font-size: 18px; 111 | margin-bottom: 5px; 112 | } 113 | .form-group:last-child { margin-bottom: 0; } 114 | } 115 | 116 | .tos { 117 | font-weight: normal; 118 | margin-bottom: 0; 119 | margin-top: 3px; 120 | input { 121 | margin-top: 0; 122 | margin-right: 3px; 123 | } 124 | } 125 | 126 | .login-link { 127 | font-size: 12px; 128 | padding-top: 4px; 129 | } 130 | 131 | .navbar { 132 | border-radius: 3px; 133 | margin-top: 8px; 134 | cursor: default; 135 | .navbar-header > .navbar-text { 136 | margin-left: 0; 137 | } 138 | .navbar-collapse { &.in, &.collapsing { 139 | padding-bottom: 16px; 140 | text-align: center; 141 | } } 142 | span.sep { 143 | color: #444; 144 | } 145 | a, a.navbar-link { 146 | color: $brand-primary + #333; 147 | &:hover, &:active, &:focus { 148 | color: white; 149 | text-decoration: none; 150 | } 151 | } 152 | } 153 | 154 | .list-group { 155 | span.nowrap { 156 | white-space: nowrap; 157 | } 158 | .list-group-item.active a { 159 | color: #F8F8F8; 160 | &:hover { color: #FFF; } 161 | } 162 | } 163 | 164 | .list-group.users { 165 | margin-bottom: 20px; 166 | .list-group-item { 167 | strong { font-size: larger; } 168 | .nudge-small-text { margin-top: 2px; } 169 | } 170 | } 171 | 172 | .panel.home .panel-body { 173 | text-align: center; 174 | padding: 10%; 175 | h1 { 176 | font-size: 35px; 177 | margin-bottom: 25px; 178 | } 179 | .buttons { 180 | margin-top: 10px; 181 | a.start { 182 | padding-left: 75px; 183 | padding-right: 75px; 184 | } 185 | } 186 | } 187 | .panel.not-connected { 188 | .panel-body { 189 | padding: 20px; 190 | text-align: center; 191 | } 192 | } 193 | 194 | .needed { 195 | .buttons { text-align: center; } 196 | select { 197 | display: inline; 198 | width: auto; 199 | } 200 | } 201 | 202 | .account-status table { 203 | display: inline-block; 204 | vertical-align: top; 205 | 206 | .label { 207 | background: transparent; 208 | font-weight: normal; 209 | font-size: 14px; 210 | // padding: 5px 10px; 211 | } 212 | .label-primary { color: $brand-primary; } 213 | .label-danger { color: $brand-danger; } 214 | } 215 | 216 | // footer handling 217 | html, body { 218 | height: 100%; 219 | } 220 | 221 | .container { 222 | // footer push 223 | min-height: 100%; 224 | height: auto !important; 225 | height: 100%; 226 | margin-bottom: -75px; 227 | } 228 | 229 | footer { 230 | height: 75px; 231 | width: 100%; 232 | 233 | .inner { 234 | color: #888; 235 | font-size: 12px; 236 | // height: 16px; 237 | line-height: 16px; 238 | text-align: center; 239 | a.stripe-logo { 240 | img { margin: 8px auto 6px auto; } 241 | } 242 | a { 243 | color: $text-color; 244 | font-weight: bold; 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include ApplicationHelper 3 | 4 | protect_from_forgery with: :exception 5 | 6 | protected 7 | 8 | # A simple before_action to redirect a non-logged-in 9 | # user to the login page 10 | def require_user 11 | if session[:user_id].blank? 12 | redirect_to new_sessions_path 13 | return 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | 3 | # The home/intro page of the application. 4 | def index 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/hooks_controller.rb: -------------------------------------------------------------------------------- 1 | class HooksController < ApplicationController 2 | # Webhooks can't possibly know our CSRF token. 3 | # So disable that feature entirely for this controller. 4 | skip_before_action :verify_authenticity_token 5 | 6 | def stripe 7 | # If the request has a 'user_id' key, then this is a webhook 8 | # event sent regarding a connected user, and not to a webhook 9 | # handler setup on the application owner's account. 10 | # So, use the user_id to look up a connected user on our end. 11 | user = params[:user_id] && User.find_by( stripe_user_id: params[:user_id] ) 12 | 13 | # If we didn't find a user, we'll have nil instead 14 | # so build our arguments to the Event API taking that into account. 15 | # We'll end up with one of: 16 | # args = [ 'EVENT_ID' ] 17 | # args = [ 'EVENT_ID', 'ACCESS_TOKEN' ] 18 | args = [ params[:id], user.try(:secret_key) ].compact 19 | 20 | # Retrieve the event from Stripe so that we can be 21 | # sure it wasn't spoofed/faked by someone being mean. 22 | begin 23 | event = Stripe::Event.retrieve( *args ) 24 | rescue Stripe::InvalidRequestError 25 | # The event doesn't exist for some reason... this might 26 | # happen if you've got other apps maybe? 27 | render nothing: true, status: 200 28 | return 29 | rescue Stripe::AuthenticationError 30 | # If we get an authentication error, and the event belongs to 31 | # a user, that means the account deauthorized 32 | # our application. We can't look up and verify the event 33 | # because the event belongs to the connected account, and we're 34 | # no longer authorized to access their account! 35 | if user && user.connected? 36 | connector = StripeConnect.new( user ) 37 | connector.deauthorized 38 | end 39 | 40 | render nothing: true, status: 200 41 | return 42 | end 43 | 44 | # Here we're actually done, but if you wanted to handle 45 | # other events (charges or invoice payment failures, etc) 46 | # then this is how you would do it. 47 | case event.try(:type) 48 | 49 | when 'account.application.deauthorized' 50 | # This is what the account.application.deauthorized 51 | # handler will hopefully look like some day, where 52 | # the event is still accessible somehow and we verified 53 | # it came from Stripe. 54 | if user && user.connected? 55 | user.manager.deauthorized 56 | end 57 | 58 | when 'account.updated' 59 | # This webhook is used for standalone and managed 60 | # accounts. It will notify you about new information 61 | # required for the account to remain in good standing. 62 | if user && user.connected? 63 | # we don't actually need to pass the event here 64 | # we'll request the account details directly inside 65 | # the manager 66 | user.manager.update_account! 67 | end 68 | 69 | # These others simply log the event because there's 70 | # not much to do with them for this example app. 71 | # You might do more useful things like sending receipt emails, etc. 72 | # Of course you can handle as many event types as you need: 73 | # https://stripe.com/docs/api#event_types 74 | when 'charge.succeeded' 75 | Rails.logger.info "**** STRIPE EVENT **** #{event.type} **** #{event.id}" 76 | when 'invoice.payment_succeeded' 77 | Rails.logger.info "**** STRIPE EVENT **** #{event.type} **** #{event.id}" 78 | when 'invoice.payment_failed' 79 | Rails.logger.info "**** STRIPE EVENT **** #{event.type} **** #{event.id}" 80 | 81 | end 82 | 83 | # We just need to respond in the affirmative. 84 | # No body is necessary. 85 | render nothing: true, status: 200 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | 3 | # Basic login page. 4 | # app/views/sessions/new.html.haml 5 | def new 6 | end 7 | 8 | # Standard basic login feature. Sets a session 9 | # key of 'user_id' with the authenticated user's 10 | # ID if they supply the correct credentials. 11 | def create 12 | user = User.find_by( email: params[:email].downcase ) 13 | if user && user.authenticate( params[:password] ) 14 | session[:user_id] = user.id 15 | redirect_to users_path 16 | else 17 | flash[:error] = "Invalid email or password :(" 18 | render action: 'new' 19 | end 20 | end 21 | 22 | # Logout... 23 | def destroy 24 | session.delete :user_id 25 | redirect_to root_path 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/stripe_controller.rb: -------------------------------------------------------------------------------- 1 | class StripeController < ApplicationController 2 | 3 | # Create a manage Stripe account for yourself. 4 | # Only works on the currently logged in user. 5 | # See app/services/stripe_managed.rb for details. 6 | def managed 7 | connector = StripeManaged.new( current_user ) 8 | account = connector.create_account!( 9 | params[:country], params[:tos] == 'on', request.remote_ip 10 | ) 11 | 12 | if account 13 | flash[:notice] = "Managed Stripe account created! View in dashboard »" 14 | else 15 | flash[:error] = "Unable to create Stripe account!" 16 | end 17 | redirect_to user_path( current_user ) 18 | end 19 | 20 | # Create a standalone Stripe account for yourself. 21 | # Only works on the currently logged in user. 22 | # See app/services/stripe_unmanaged.rb for details. 23 | def standalone 24 | connector = StripeStandalone.new( current_user ) 25 | account = connector.create_account!( params[:country] ) 26 | 27 | if account 28 | flash[:notice] = "Standalone Stripe account created! View in dashboard »" 29 | else 30 | flash[:error] = "Unable to create Stripe account!" 31 | end 32 | redirect_to user_path( current_user ) 33 | end 34 | 35 | # Connect yourself to a Stripe account. 36 | # Only works on the currently logged in user. 37 | # See app/services/stripe_oauth.rb for #oauth_url details. 38 | def oauth 39 | connector = StripeOauth.new( current_user ) 40 | url, error = connector.oauth_url( redirect_uri: stripe_confirm_url ) 41 | 42 | if url.nil? 43 | flash[:error] = error 44 | redirect_to user_path( current_user ) 45 | else 46 | redirect_to url 47 | end 48 | end 49 | 50 | # Confirm a connection to a Stripe account. 51 | # Only works on the currently logged in user. 52 | # See app/services/stripe_connect.rb for #verify! details. 53 | def confirm 54 | connector = StripeOauth.new( current_user ) 55 | if params[:code] 56 | # If we got a 'code' parameter. Then the 57 | # connection was completed by the user. 58 | connector.verify!( params[:code] ) 59 | 60 | elsif params[:error] 61 | # If we have an 'error' parameter, it's because the 62 | # user denied the connection request. Other errors 63 | # are handled at #oauth_url generation time. 64 | flash[:error] = "Authorization request denied." 65 | end 66 | 67 | redirect_to user_path( current_user ) 68 | end 69 | 70 | # Deauthorize the application from accessing 71 | # the connected Stripe account. 72 | # Only works on the currently logged in user. 73 | def deauthorize 74 | connector = StripeOauth.new( current_user ) 75 | connector.deauthorize! 76 | flash[:notice] = "Account disconnected from Stripe." 77 | redirect_to user_path( current_user ) 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | # Most actions here need a logged in user. 3 | # ApplicationHelper#current_user will return the logged in user. 4 | before_action :require_user, except: %w{ new create } 5 | 6 | # A list of all users in the database. 7 | # app/views/users/index.html.haml 8 | def index 9 | @users = User.all 10 | end 11 | 12 | # A signup form. 13 | # app/views/users/new.html.haml 14 | def new 15 | @user = User.new 16 | end 17 | 18 | # Create a new user via #new 19 | # Log them in after creation, and take 20 | # them to their own 'profile page'. 21 | def create 22 | @user = User.create( user_params ) 23 | session[:user_id] = @user.id 24 | if @user.valid? 25 | redirect_to user_path( @user ) 26 | else 27 | render action: 'new' 28 | end 29 | end 30 | 31 | def update 32 | manager = current_user.manager 33 | manager.update_account! params: params 34 | redirect_to user_path( current_user ) 35 | end 36 | 37 | # Show a user's profile page. 38 | # This is where you can spend money with the connected account. 39 | # app/views/users/show.html.haml 40 | def show 41 | @user = User.find( params[:id] ) 42 | @plans = Stripe::Plan.all 43 | end 44 | 45 | # Make a one-off payment to the user. 46 | # See app/assets/javascripts/app/pay.coffee 47 | def pay 48 | # Find the user to pay. 49 | user = User.find( params[:id] ) 50 | 51 | # Charge $10. 52 | amount = 1000 53 | # Calculate the fee amount that goes to the application. 54 | fee = (amount * Rails.application.secrets.fee_percentage).to_i 55 | 56 | begin 57 | charge_attrs = { 58 | amount: amount, 59 | currency: user.currency, 60 | source: params[:token], 61 | description: "Test Charge via Stripe Connect", 62 | application_fee: fee 63 | } 64 | 65 | case params[:charge_on] 66 | when 'connected' 67 | # Use the user-to-be-paid's access token 68 | # to make the charge directly on their account 69 | charge = Stripe::Charge.create( charge_attrs, user.secret_key ) 70 | when 'platform' 71 | # Use the platform's access token, and specify the 72 | # connected account's user id as the destination so that 73 | # the charge is transferred to their account. 74 | charge_attrs[:destination] = user.stripe_user_id 75 | charge = Stripe::Charge.create( charge_attrs ) 76 | end 77 | 78 | flash[:notice] = "Charged successfully! View in dashboard »" 79 | 80 | rescue Stripe::CardError => e 81 | error = e.json_body[:error][:message] 82 | flash[:error] = "Charge failed! #{error}" 83 | end 84 | 85 | redirect_to user_path( user ) 86 | end 87 | 88 | # Subscribe the currently logged in user to 89 | # a plan owned by the application. 90 | # See app/assets/javascripts/app/subscribe.coffee 91 | def subscribe 92 | # Find the user to pay. 93 | user = User.find( params[:id] ) 94 | 95 | # Calculate the fee percentage that applies to 96 | # all invoices for this subscription. 97 | fee_percent = (Rails.application.secrets.fee_percentage * 100).to_i 98 | begin 99 | # Create a customer and subscribe them to a plan 100 | # in one shot. 101 | # Normally after this you would store customer.id 102 | # in your database so that you can keep track of 103 | # the subscription status/etc. Here we're just 104 | # fire-and-forgetting it. 105 | customer = Stripe::Customer.create( 106 | { 107 | source: params[:token], 108 | email: current_user.email, 109 | plan: params[:plan], 110 | application_fee_percent: fee_percent 111 | }, 112 | user.secret_key 113 | ) 114 | flash[:notice] = "Subscribed! View in dashboard »" 115 | 116 | rescue Stripe::CardError => e 117 | error = e.json_body[:error][:message] 118 | flash[:error] = "Charge failed! #{error}" 119 | end 120 | 121 | redirect_to user_path( user ) 122 | end 123 | 124 | private 125 | 126 | def user_params 127 | p = params.require(:new_user).permit( :name, :email, :password ) 128 | p[:email].downcase! 129 | p 130 | end 131 | 132 | end 133 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | 3 | # Lookup logged in user from session, if applicable. 4 | def current_user 5 | @_current_user ||= User.find_by_id( session[:user_id] ) 6 | end 7 | 8 | # Simply checks if the @user instance variable 9 | # is the current user. Used to check if we're 10 | # looking our own profile page, basically. 11 | # See app/views/users/show.html.haml 12 | def is_myself? 13 | @user == current_user 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | validates :email, presence: true, uniqueness: true 3 | validates :name, presence: true 4 | serialize :stripe_account_status, JSON 5 | has_secure_password 6 | 7 | # General 'has a Stripe account' check 8 | def connected?; !stripe_user_id.nil?; end 9 | 10 | # Stripe account type checks 11 | def managed?; stripe_account_type == 'managed'; end 12 | def standalone?; stripe_account_type == 'standalone'; end 13 | def oauth?; stripe_account_type == 'oauth'; end 14 | 15 | def manager 16 | case stripe_account_type 17 | when 'managed' then StripeManaged.new(self) 18 | when 'standalone' then StripeStandalone.new(self) 19 | when 'oauth' then StripeOauth.new(self) 20 | end 21 | end 22 | 23 | def can_accept_charges? 24 | return true if oauth? 25 | return true if managed? && stripe_account_status['charges_enabled'] 26 | return true if standalone? && stripe_account_status['charges_enabled'] 27 | return false 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/services/stripe_managed.rb: -------------------------------------------------------------------------------- 1 | class StripeManaged < Struct.new( :user ) 2 | ALLOWED = [ 'US', 'CA' ] # public beta 3 | COUNTRIES = [ 4 | { name: 'United States', code: 'US' }, 5 | { name: 'Canada', code: 'CA' }, 6 | { name: 'Australia', code: 'AU' }, 7 | { name: 'United Kingdom', code: 'GB' }, 8 | { name: 'Ireland', code: 'IE' } 9 | ] 10 | 11 | def create_account!( country, tos_accepted, ip ) 12 | return nil unless tos_accepted 13 | return nil unless country.in?( COUNTRIES.map { |c| c[:code] } ) 14 | 15 | begin 16 | @account = Stripe::Account.create( 17 | managed: true, 18 | country: country, 19 | email: user.email, 20 | tos_acceptance: { 21 | ip: ip, 22 | date: Time.now.to_i 23 | }, 24 | legal_entity: { 25 | type: 'individual', 26 | } 27 | ) 28 | rescue 29 | nil # TODO: improve 30 | end 31 | 32 | if @account 33 | user.update_attributes( 34 | currency: @account.default_currency, 35 | stripe_account_type: 'managed', 36 | stripe_user_id: @account.id, 37 | secret_key: @account.keys.secret, 38 | publishable_key: @account.keys.publishable, 39 | stripe_account_status: account_status 40 | ) 41 | end 42 | 43 | @account 44 | end 45 | 46 | def update_account!( params: nil ) 47 | if params 48 | if params[:bank_account_token] 49 | account.bank_account = params[:bank_account_token] 50 | account.save 51 | end 52 | 53 | if params[:legal_entity] 54 | # clean up dob fields 55 | params[:legal_entity][:dob] = { 56 | year: params[:legal_entity].delete('dob(1i)'), 57 | month: params[:legal_entity].delete('dob(2i)'), 58 | day: params[:legal_entity].delete('dob(3i)') 59 | } 60 | 61 | # update legal_entity hash from the params 62 | params[:legal_entity].entries.each do |key, value| 63 | if [ :address, :dob ].include? key.to_sym 64 | value.entries.each do |akey, avalue| 65 | next if avalue.blank? 66 | # Rails.logger.error "#{akey} - #{avalue.inspect}" 67 | account.legal_entity[key] ||= {} 68 | account.legal_entity[key][akey] = avalue 69 | end 70 | else 71 | next if value.blank? 72 | # Rails.logger.error "#{key} - #{value.inspect}" 73 | account.legal_entity[key] = value 74 | end 75 | end 76 | 77 | # copy 'address' as 'personal_address' 78 | pa = account.legal_entity['address'].dup.to_h 79 | account.legal_entity['personal_address'] = pa 80 | 81 | account.save 82 | end 83 | end 84 | 85 | user.update_attributes( 86 | stripe_account_status: account_status 87 | ) 88 | end 89 | 90 | def legal_entity 91 | account.legal_entity 92 | end 93 | 94 | def needs?( field ) 95 | user.stripe_account_status['fields_needed'].grep( Regexp.new( /#{field}/i ) ).any? 96 | end 97 | 98 | def supported_bank_account_countries 99 | country_codes = case account.country 100 | when 'US' then %w{ US } 101 | when 'CA' then %w{ US CA } 102 | when 'IE', 'UK' then %w{ IE UK US } 103 | when 'AU' then %w{ AU } 104 | end 105 | COUNTRIES.select do |country| 106 | country[:code].in? country_codes 107 | end 108 | end 109 | 110 | protected 111 | 112 | def account_status 113 | { 114 | details_submitted: account.details_submitted, 115 | charges_enabled: account.charges_enabled, 116 | transfers_enabled: account.transfers_enabled, 117 | fields_needed: account.verification.fields_needed, 118 | due_by: account.verification.due_by 119 | } 120 | end 121 | 122 | def account 123 | @account ||= Stripe::Account.retrieve( user.stripe_user_id ) 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /app/services/stripe_oauth.rb: -------------------------------------------------------------------------------- 1 | class StripeOauth < Struct.new( :user ) 2 | 3 | def oauth_url( params ) 4 | url = client.authorize_url( { 5 | scope: 'read_write', 6 | stripe_landing: 'login', 7 | stripe_user: { 8 | email: user.email 9 | } 10 | }.merge( params ) ) 11 | 12 | # Make a request to this URL by hand before 13 | # redirecting the user there. This way we 14 | # can handle errors (other than access_denied, which 15 | # could come later). 16 | # See https://stripe.com/docs/connect/reference#get-authorize-errors 17 | begin 18 | response = RestClient.get url 19 | # If the request was successful, then we're all good to return 20 | # this URL. 21 | 22 | rescue => e 23 | # On the other hand, if the request failed, then 24 | # we can't send them to connect. 25 | json = JSON.parse(e.response.body) rescue nil 26 | if json && json['error'] 27 | case json['error'] 28 | 29 | # The application is configured incorrectly and 30 | # does not have the right Redirect URI 31 | when 'invalid_redirect_uri' 32 | return nil, <<-EOF 33 | Redirect URI is not setup correctly. 34 | Please see the README. 35 | EOF 36 | 37 | # Something else horrible happened? Network is down, 38 | # Stripe API is broken?... 39 | else 40 | return [ nil, params[:error_description] ] 41 | 42 | end 43 | end 44 | 45 | # If there was some problem parsing the body 46 | # or there's no 'error' parameter, then something 47 | # _really_ went wrong. So just blow up here. 48 | return [ nil, "Unable to connect to Stripe. #{e.message}" ] 49 | end 50 | 51 | [ url, nil ] 52 | end 53 | 54 | # Upon redirection back to this app, we'll have 55 | # a 'code' that we can use to get the access token 56 | # and other details about our connected user. 57 | # See app/controllers/users_controller.rb#confirm for counterpart. 58 | def verify!( code ) 59 | data = client.get_token( code, { 60 | headers: { 61 | 'Authorization' => "Bearer #{Rails.application.secrets.stripe_secret_key}" 62 | } 63 | } ) 64 | 65 | user.stripe_user_id = data.params['stripe_user_id'] 66 | user.stripe_account_type = 'oauth' 67 | user.publishable_key = data.params['stripe_publishable_key'] 68 | user.secret_key = data.token 69 | user.currency = default_currency 70 | 71 | user.save! 72 | end 73 | 74 | # Deauthorize the user. Straight-forward enough. 75 | # See app/controllers/users_controller.rb#deauthorize for counterpart. 76 | def deauthorize! 77 | response = RestClient.post( 78 | 'https://connect.stripe.com/oauth/deauthorize', 79 | { client_id: Rails.application.secrets.stripe_client_id, 80 | stripe_user_id: user.stripe_user_id }, 81 | { 'Authorization' => "Bearer #{Rails.application.secrets.stripe_secret_key}" } 82 | ) 83 | user_id = JSON.parse( response.body )['stripe_user_id'] 84 | 85 | deauthorized if response.code == 200 && user_id == user.stripe_user_id 86 | end 87 | 88 | # The actual deauthorization on our side consists 89 | # of 'forgetting' the now-invalid user_id, API keys, etc. 90 | # Used here in #deauthorize! as well as in the webhook handler: 91 | # app/controllers/hooks_controller.rb#stripe 92 | def deauthorized 93 | user.update_attributes( 94 | stripe_user_id: nil, 95 | secret_key: nil, 96 | publishable_key: nil, 97 | currency: nil 98 | ) 99 | end 100 | 101 | private 102 | 103 | # Get the default currency of the connected user. 104 | # All transactions will use this currency. 105 | def default_currency 106 | Stripe::Account.retrieve( user.stripe_user_id, user.secret_key ).default_currency 107 | end 108 | 109 | # A simple OAuth2 client we can use to generate a URL 110 | # to redirect the user to as well as get an access token. 111 | # Used in #oauth_url and #verify! 112 | def client 113 | @client ||= OAuth2::Client.new( 114 | Rails.application.secrets.stripe_client_id, 115 | Rails.application.secrets.stripe_secret_key, 116 | { 117 | site: 'https://connect.stripe.com', 118 | authorize_url: '/oauth/authorize', 119 | token_url: '/oauth/token' 120 | } 121 | ).auth_code 122 | end 123 | 124 | end 125 | -------------------------------------------------------------------------------- /app/services/stripe_standalone.rb: -------------------------------------------------------------------------------- 1 | class StripeStandalone < Struct.new( :user ) 2 | COUNTRIES = [ 3 | { name: 'United States', code: 'US' }, 4 | { name: 'Canada', code: 'CA' }, 5 | { name: 'Australia', code: 'AU' }, 6 | { name: 'United Kingdom', code: 'GB' }, 7 | { name: 'Ireland', code: 'IE' } 8 | ] 9 | 10 | def create_account!( country ) 11 | return nil unless country.in?( COUNTRIES.map { |c| c[:code] } ) 12 | 13 | begin 14 | @account = Stripe::Account.create( 15 | email: user.email, 16 | managed: false, 17 | country: country 18 | ) 19 | rescue 20 | nil # TODO: improve 21 | end 22 | 23 | if @account 24 | user.update_attributes( 25 | currency: @account.default_currency, 26 | stripe_account_type: 'standalone', 27 | stripe_user_id: @account.id, 28 | secret_key: @account.keys.secret, 29 | publishable_key: @account.keys.publishable, 30 | stripe_account_status: account_status 31 | ) 32 | end 33 | 34 | @account 35 | end 36 | 37 | protected 38 | 39 | def account_status 40 | { 41 | details_submitted: account.details_submitted, 42 | charges_enabled: account.charges_enabled, 43 | transfers_enabled: account.transfers_enabled 44 | } 45 | end 46 | 47 | def account 48 | @account ||= Stripe::Account.retrieve( user.stripe_user_id ) 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /app/views/home/index.html.haml: -------------------------------------------------------------------------------- 1 | - fresh = !User.any? 2 | 3 | %br 4 | .col-md-8.col-md-offset-2.col-xs-12 5 | - if flash[:notice] 6 | .alert.alert-info.auto 7 | %p= flash[:notice] 8 | 9 | .panel.panel-primary.home 10 | .panel-body 11 | %h1 Rails + Stripe Connect Example 12 | 13 | %p 14 | This application demonstrates the basics of implementing a 15 | %br 16 | %a{ href: 'https://stripe.com/docs/connect' } Stripe Connect 17 | application in Rails. 18 | 19 | .alert.alert-warning 20 | Please be sure you've read the 21 | %a{ href: "#{Rails.configuration.github_url}/blob/master/README.markdown" } README 22 | and run the 23 | = succeed '.' do 24 | %a{ href: "#{Rails.configuration.github_url}/blob/master/lib/tasks/setup.rake" } setup script 25 | 26 | .buttons 27 | - unless fresh 28 | %a.btn.btn-primary.btn-lg{ href: new_sessions_path } Login 29 | %a.btn.btn-primary.btn-lg{ class: ('start' if fresh), href: new_user_path } 30 | = fresh ? 'Get Started »'.html_safe : 'New User' 31 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title Rails Stripe Connect Example 5 | %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' } 6 | 7 | = stylesheet_link_tag 'application', media: 'all' 8 | = javascript_include_tag 'application' 9 | = csrf_meta_tags 10 | %body 11 | .container 12 | = yield 13 | 14 | %footer 15 | .inner 16 | .col-md-6.col-md-offset-3.col-xs-12 17 | .info 18 | This 19 | %a{ target: '_blank', href: Rails.configuration.github_url } open-source 20 | example application was made with 21 | %span.glyphicon.glyphicon-headphones 22 | by 23 | = succeed '.' do 24 | %a{ href: 'http://ryanfunduk.com', target: '_blank' } Ryan Funduk 25 | %a.stripe-logo{ target: '_blank', href: 'https://stripe.com' } 26 | = image_tag 'powered-by-stripe.png', width: 238 / 2, height: 52 / 2, alt: '' 27 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.haml: -------------------------------------------------------------------------------- 1 | %br 2 | .col-md-4.col-md-offset-4.col-xs-12 3 | .panel.panel-primary 4 | .panel-body 5 | = form_tag sessions_path, method: 'POST' do 6 | %h2 Login 7 | - if flash[:error] 8 | .alert.alert-danger= flash[:error] 9 | 10 | .form-group 11 | = email_field_tag :email, '', placeholder: 'me@example.com', class: 'form-control' 12 | 13 | .form-group 14 | = password_field_tag :password, '', placeholder: 'password', class: 'form-control' 15 | 16 | %button.btn.btn-primary.main-button{ type: 'submit' } Login 17 | -------------------------------------------------------------------------------- /app/views/users/_connect.html.haml: -------------------------------------------------------------------------------- 1 | .panel.panel-primary 2 | .panel-body 3 | %h3 Connect 4 | %p There are 3 ways to create/connect your Stripe account. 5 | 6 | %ul.list-group 7 | %li.list-group-item#stripe-oauth 8 | %a.pull-right.btn.btn-lg.btn-primary{ href: stripe_oauth_path } Connect 9 | %h3 OAuth 10 | %p Connect or create a Stripe account via OAuth. 11 | 12 | %li.list-group-item#stripe-standalone 13 | = form_tag stripe_standalone_path, method: 'POST' do 14 | %input.pull-right.btn.btn-lg.btn-primary{ type: 'submit', value: 'Create' } 15 | %h3 Standalone 16 | %p 17 | %small Create a standalone Stripe account in 18 | %select.country{ name: 'country' } 19 | - StripeStandalone::COUNTRIES.each do |country| 20 | %option{ value: country[:code] }= country[:name] 21 | 22 | - # managed accounts are in public beta 23 | - # see services/stripe_managed.rb#ALLOWED 24 | - if Stripe::Account.retrieve('self').country.in? StripeManaged::ALLOWED 25 | %li.list-group-item#stripe-managed 26 | = form_tag stripe_managed_path, method: 'POST' do 27 | %input.pull-right.btn.btn-lg.disabled.btn-primary{ type: 'submit', value: 'Create' } 28 | %h3 Managed 29 | %p 30 | %small Create a managed Stripe account in 31 | %select.country{ name: 'country' } 32 | - StripeManaged::COUNTRIES.each do |country| 33 | %option{ value: country[:code] }= country[:name] 34 | %br 35 | %label.tos 36 | %input{ type: 'checkbox', name: 'tos', checked: false } 37 | I accept the 38 | %a{ href: 'https://stripe.com/us/terms', target: '_blank' } Stripe Terms of Service 39 | -------------------------------------------------------------------------------- /app/views/users/_nav.html.haml: -------------------------------------------------------------------------------- 1 | %nav.navbar.navbar-inverse 2 | .container-fluid 3 | .navbar-header 4 | %button.navbar-toggle.collapsed{ type: 'button', data: { toggle: 'collapse', target: '#navbar-links' } } 5 | %span.icon-bar 6 | %span.icon-bar 7 | %span.icon-bar 8 | 9 | %p.navbar-text 10 | %span.glyphicon.glyphicon-chevron-right 11 | %span.hidden-xs Logged in as 12 | %a{ href: user_path( current_user ) } 13 | %strong= current_user.name 14 | 15 | .collapse.navbar-collapse{ id: 'navbar-links' } 16 | %p.navbar-text.navbar-right 17 | - if request.path == users_path 18 | All Users 19 | - else 20 | %a.navbar-link{ href: users_path } All Users 21 | %span.sep  |  22 | %a.navbar-link{ href: destroy_sessions_path } Logout 23 | -------------------------------------------------------------------------------- /app/views/users/_pay.html.haml: -------------------------------------------------------------------------------- 1 | - if @user.can_accept_charges? 2 | .panel.panel-primary 3 | .panel-body 4 | = form_tag pay_user_path( @user ) do 5 | .indicator 6 | = hidden_field_tag :token 7 | %button.pull-right.btn.btn-primary.btn-lg.pay-button Pay $10 8 | 9 | %h3 Make a one-off charge 10 | %p 11 | Pay a one-time charge of $10 #{@user.currency.upcase}. 12 | %p 13 | %small Responsible for fees, refunds and chargebacks? 14 | %select{ name: 'charge_on' } 15 | %option{ value: 'connected' } Connected Account 16 | %option{ value: 'platform' } Platform Account 17 | 18 | .panel.panel-primary 19 | .panel-body 20 | = form_tag subscribe_user_path( @user ) do 21 | .indicator 22 | = hidden_field_tag :token 23 | = hidden_field_tag :plan 24 | %button.pull-right.btn.btn-primary.btn-lg.subscribe-button{ disabled: true } Subscribe 25 | 26 | %h3 Subscribe to a plan 27 | %p 28 | - if @plans.any? 29 | The application defines the following plans: 30 | - else 31 | The application has no plans. 32 | %a{ href: 'https://dashboard.stripe.com/test/plans', rel: 'platform-account', target: '_blank' } Add Plans » 33 | - if @plans.any? 34 | .list-group 35 | - @plans.each do |plan| 36 | %a.list-group-item.plan-choice{ data: plan.to_hash } 37 | = plan.name 38 | \- 39 | %strong 40 | = number_to_currency( plan.amount / 100 ) 41 | = plan.currency.upcase 42 | %p 43 | %small Responsible for fees, refunds and chargebacks? 44 | %select{ name: 'charge_on', disabled: true } 45 | %option{ value: 'connected' } Connected Account 46 | - # charging on the platform is currently not 47 | - # supported for subscriptions 48 | - # %option{ value: 'platform' } Platform Account 49 | 50 | - else 51 | .panel.panel-danger 52 | .panel-body 53 | %h3 Nope 54 | 55 | = javascript_include_tag 'https://checkout.stripe.com/checkout.js' 56 | :javascript 57 | // You can select to pay either directly or via the platform, so 58 | // we need both publishable keys here. 59 | window.publishable = { 60 | platform: #{Rails.application.secrets.stripe_publishable_key.to_json}, 61 | connected: #{@user.publishable_key.to_json} 62 | }; 63 | window.currentUserEmail = #{current_user.email.to_json}; 64 | window.payPath = #{pay_user_path( @user ).to_json}; 65 | -------------------------------------------------------------------------------- /app/views/users/_settings.html.haml: -------------------------------------------------------------------------------- 1 | .panel.panel-primary 2 | .panel-body 3 | - if current_user.oauth? 4 | %a.pull-right.btn.btn-danger.btn-lg{ href: stripe_deauthorize_path } Deauthorize 5 | %h3 Disconnect 6 | %p 7 | Since this is you, you can 8 | %br 9 | disconnect from this application. 10 | 11 | - if current_user.managed? 12 | %h2 Stripe Account Management 13 | %p 14 | Account Type: 15 | %strong Managed 16 | 17 | - # the current account status, whether the 18 | - # account can make charges or can receive transfers 19 | %p 20 | .account-status 21 | Status: 22 | - charges = [ 'Charges', @user.stripe_account_status['charges_enabled'] ] 23 | - transfers = [ 'Transfers', @user.stripe_account_status['transfers_enabled'] ] 24 | %table 25 | - [ charges, transfers ].each do |text, yn| 26 | %tr 27 | %td 28 | %span.label{ class: yn ? 'label-primary' : 'label-danger' } 29 | %span.glyphicon{ class: yn ? 'glyphicon-ok' : 'glyphicon-remove' } 30 | = text 31 | 32 | - # if we need more information from this user to keep 33 | - # their account in good standing 34 | - if @user.stripe_account_status['fields_needed'].any? 35 | %hr 36 | .needed 37 | = form_for @user, html: { class: 'form-horizontal' } do |f| 38 | %h3 Needed Information 39 | - if @user.stripe_account_status['due_by'] 40 | %p 41 | Due by: 42 | %strong= Time.at( @user.stripe_account_status['due_by'] ).strftime("%Y-%m-%d") 43 | 44 | - if params[:debug] 45 | = debug @user.stripe_account_status['fields_needed'] 46 | 47 | - manager = StripeManaged.new( current_user ) 48 | %ul.list-group 49 | 50 | - # this account needs a bank account 51 | - if manager.needs? 'bank_account' 52 | %li.list-group-item#bank-account{ data: { publishable: @user.publishable_key } } 53 | %script{ src: 'https://js.stripe.com/v2/' } 54 | %h3 Bank Account 55 | = hidden_field_tag :bank_account_token 56 | .form-group 57 | - countries = manager.supported_bank_account_countries 58 | %label.control-label.col-xs-12.col-sm-3 Country: 59 | .col-xs-12.col-sm-9 60 | %select.form-control{ data: { stripe: 'country' } } 61 | - countries.each do |country| 62 | %option{ value: country[:code] }= country[:name] 63 | .form-group 64 | %label.control-label.col-xs-12.col-sm-3 Account Number: 65 | .col-xs-12.col-sm-9 66 | %input.form-control{ type: 'text', data: { stripe: 'account_number' } } 67 | .form-group#bank-routing-container 68 | %label.control-label.col-xs-12.col-sm-3 Routing Number: 69 | .col-xs-12.col-sm-9 70 | %input.form-control{ type: 'text', data: { stripe: 'routing_number' } } 71 | 72 | - # this account needs legal entity info 73 | - if manager.needs? 'legal_entity.' 74 | %li.list-group-item 75 | %h3 Legal Entity 76 | .form-group 77 | %label.control-label.col-xs-12.col-sm-3 First Name: 78 | .col-xs-12.col-sm-9 79 | %input.form-control{ type: 'text', name: 'legal_entity[first_name]', value: manager.legal_entity.first_name } 80 | 81 | .form-group 82 | %label.control-label.col-xs-12.col-sm-3 Last Name: 83 | .col-xs-12.col-sm-9 84 | %input.form-control{ type: 'text', name: 'legal_entity[last_name]', value: manager.legal_entity.last_name } 85 | 86 | .form-group 87 | %label.control-label.col-xs-12.col-sm-3 Date of Birth: 88 | .col-xs-12.col-sm-9 89 | - dob = manager.legal_entity.dob 90 | - selected = Date.new( dob.year, dob.month, dob.day ) rescue nil 91 | = date_select 'legal_entity', 'dob', 92 | { selected: selected, 93 | prompt: true, 94 | start_year: 90.years.ago.year, 95 | end_year: 13.years.ago.year }, 96 | { class: 'form-control' } 97 | 98 | - if manager.needs? 'legal_entity.personal_id_number' 99 | .form-group 100 | %label.control-label.col-xs-12.col-sm-3 Personal ID Number: 101 | .col-xs-12.col-sm-9 102 | %input.form-control{ type: 'text', name: 'legal_entity[personal_id_number]' } 103 | - elsif manager.needs? 'legal_entity.ssn_last_4' 104 | .form-group 105 | %label.control-label.col-xs-12.col-sm-3 SSN Last 4: 106 | .col-xs-12.col-sm-9 107 | %input.form-control{ type: 'text', name: 'legal_entity[ssn_last_4]' } 108 | 109 | .form-group 110 | %label.control-label.col-xs-12.col-sm-3 Address Line 1: 111 | .col-xs-12.col-sm-9 112 | %input.form-control{ type: 'text', name: 'legal_entity[address][line1]', value: manager.legal_entity.address.line1 } 113 | 114 | .form-group 115 | %label.control-label.col-xs-12.col-sm-3 Address Line 2: 116 | .col-xs-12.col-sm-9 117 | %input.form-control{ type: 'text', name: 'legal_entity[address][line2]', value: manager.legal_entity.address.line2 } 118 | 119 | .form-group 120 | %label.control-label.col-xs-12.col-sm-3 City: 121 | .col-xs-12.col-sm-9 122 | %input.form-control{ type: 'text', name: 'legal_entity[address][city]', value: manager.legal_entity.address.city } 123 | 124 | .form-group 125 | %label.control-label.col-xs-12.col-sm-3 State/Province: 126 | .col-xs-12.col-sm-9 127 | %input.form-control{ type: 'text', name: 'legal_entity[address][state]', value: manager.legal_entity.address.state } 128 | 129 | .form-group 130 | %label.control-label.col-xs-12.col-sm-3 ZIP/Postal Code: 131 | .col-xs-12.col-sm-9 132 | %input.form-control{ type: 'text', name: "legal_entity[address][postal_code]", value: manager.legal_entity.address.postal_code } 133 | 134 | 135 | %br 136 | .buttons 137 | = f.submit 'Save Info', class: 'btn btn-lg btn-primary' 138 | 139 | - if current_user.standalone? 140 | %h3 Stripe Account Management 141 | %p 142 | Account Type: 143 | %strong Standalone 144 | 145 | - # the current account status, whether the 146 | - # account can make charges or can receive transfers 147 | %p 148 | .account-status 149 | Status: 150 | - charges = [ 'Charges', @user.stripe_account_status['charges_enabled'] ] 151 | - transfers = [ 'Transfers', @user.stripe_account_status['transfers_enabled'] ] 152 | %table 153 | - [ charges, transfers ].each do |text, yn| 154 | %tr 155 | %td 156 | %span.label{ class: yn ? 'label-primary' : 'label-danger' } 157 | %span.glyphicon{ class: yn ? 'glyphicon-ok' : 'glyphicon-remove' } 158 | = text 159 | - unless @user.stripe_account_status['details_submitted'] 160 | %hr 161 | %ul.list-group 162 | %li.list-group-item 163 | %a.btn.btn-primary.btn-lg.pull-right{ href: 'https://dashboard.stripe.com/account/details' } 164 | %span.glyphicon.glyphicon-arrow-right 165 | %h3 Claim or activate your account. 166 | %p 167 | You will also receive a link from Stripe via email. 168 | -------------------------------------------------------------------------------- /app/views/users/index.html.haml: -------------------------------------------------------------------------------- 1 | .col-md-6.col-md-offset-3.col-xs-12 2 | = render partial: 'nav' 3 | 4 | %h1 Users 5 | 6 | %br 7 | 8 | .list-group.users 9 | - @users.each do |user| 10 | .list-group-item{ class: ('active' if user == current_user), href: user_path( user ) } 11 | .pull-right.nudge-small-text 12 | %small 13 | %a{ target: '_blank', rel: 'platform-account', href: "https://dashboard.stripe.com/test/applications/users/#{user.stripe_user_id}" }= user.stripe_user_id 14 | %a{ href: user_path( user ) } 15 | %strong= user.name 16 | %span.nowrap (#{user.email}) 17 | -------------------------------------------------------------------------------- /app/views/users/new.html.haml: -------------------------------------------------------------------------------- 1 | %br 2 | .col-md-4.col-md-offset-4.col-xs-12 3 | .panel.panel-primary 4 | .panel-body 5 | = form_for @user, as: 'new_user', html: { class: 'form-horizontal' } do |f| 6 | %h2 New User 7 | - if @user.errors.any? 8 | .alert.alert-danger 9 | %strong Found some problems :( 10 | %ul 11 | - @user.errors.full_messages.each do |msg| 12 | %li= msg 13 | 14 | .form-group 15 | = f.label :name, class: 'col-xs-3 control-label' 16 | .col-xs-9 17 | = f.text_field :name, class: 'form-control', placeholder: 'Your Name' 18 | .form-group 19 | = f.label :email, class: 'col-xs-3 control-label' 20 | .col-xs-9 21 | = f.email_field :email, class: 'form-control', placeholder: 'your@email.com' 22 | .form-group 23 | = f.label :password, class: 'col-xs-3 control-label' 24 | .col-xs-9 25 | = f.password_field :password, class: 'form-control', placeholder: ('•' * 16).html_safe 26 | 27 | %button.btn.btn-primary.main-button{ type: 'submit' } Signup 28 | -------------------------------------------------------------------------------- /app/views/users/show.html.haml: -------------------------------------------------------------------------------- 1 | .col-md-6.col-md-offset-3.col-xs-12 2 | = render partial: 'nav' 3 | 4 | - if flash[:notice] 5 | .alert.alert-info 6 | %p= flash[:notice].html_safe 7 | - if flash[:error] 8 | .alert.alert-danger 9 | %p= flash[:error].html_safe 10 | 11 | %h1= @user.name 12 | %h4= @user.email 13 | 14 | - if @user.connected? 15 | - if is_myself? 16 | - # you're looking at your own 'profile', so you can 17 | - # update/deauthorize/etc your Stripe account 18 | = render partial: 'settings' 19 | - else 20 | = render partial: 'pay' 21 | 22 | - else 23 | - if is_myself? && !current_user.connected? 24 | - # you're looking at your own 'profile', so you can 25 | - # create/connect/etc your Stripe account 26 | = render partial: 'connect' 27 | - else 28 | .panel.panel-danger.not-connected 29 | .panel-body 30 | %h3 Not Connected 31 | %p 32 | This user is not connected to Stripe, so 33 | you can't pay them. 34 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_model/railtie" 5 | require "active_record/railtie" 6 | require "action_controller/railtie" 7 | require "action_mailer/railtie" 8 | require "action_view/railtie" 9 | require "sprockets/railtie" 10 | # require "rails/test_unit/railtie" 11 | 12 | # Require the gems listed in Gemfile, including any gems 13 | # you've limited to :test, :development, or :production. 14 | Bundler.require(*Rails.groups) 15 | 16 | module RailsStripeConnectExample 17 | class Application < Rails::Application 18 | config.github_url = 'https://github.com/rfunduk/rails-stripe-connect-example' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/rest_client.rb: -------------------------------------------------------------------------------- 1 | RestClient.log = Rails.logger 2 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_rails-stripe-connect-example_session' 4 | -------------------------------------------------------------------------------- /config/initializers/stripe.rb: -------------------------------------------------------------------------------- 1 | Stripe.api_key = Rails.application.secrets.stripe_secret_key 2 | Stripe.api_version = '2015-04-07' 3 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | root to: 'home#index' 3 | 4 | resources :users do 5 | member do 6 | post :pay 7 | post :subscribe 8 | end 9 | end 10 | 11 | resource :sessions do 12 | member do 13 | get :destroy, as: 'destroy' 14 | end 15 | end 16 | 17 | # Stripe Connect endpoints 18 | # - oauth flow 19 | get '/connect/oauth' => 'stripe#oauth', as: 'stripe_oauth' 20 | get '/connect/confirm' => 'stripe#confirm', as: 'stripe_confirm' 21 | get '/connect/deauthorize' => 'stripe#deauthorize', as: 'stripe_deauthorize' 22 | # - create accounts 23 | post '/connect/managed' => 'stripe#managed', as: 'stripe_managed' 24 | post '/connect/standalone' => 'stripe#standalone', as: 'stripe_standalone' 25 | 26 | # Stripe webhooks 27 | post '/hooks/stripe' => 'hooks#stripe' 28 | end 29 | -------------------------------------------------------------------------------- /config/secrets.sample.yml: -------------------------------------------------------------------------------- 1 | development: 2 | secret_key_base: __RAILS_SECRET 3 | stripe_publishable_key: __STRIPE_PUBLISHABLE 4 | stripe_secret_key: __STRIPE_SECRET 5 | stripe_client_id: __STRIPE_CLIENT 6 | fee_percentage: 0.1 7 | 8 | test: 9 | secret_key_base: __RAILS_SECRET 10 | -------------------------------------------------------------------------------- /db/migrate/20141130003114_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email 6 | t.string :password_digest 7 | 8 | t.string :publishable_key 9 | t.string :secret_key 10 | t.string :stripe_user_id 11 | t.string :currency 12 | 13 | t.timestamps 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20150310212328_add_stripe_account_type_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddStripeAccountTypeToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :stripe_account_type, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20150318194136_add_managed_account_status_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddManagedAccountStatusToUsers < ActiveRecord::Migration 2 | def change 3 | add_column :users, :stripe_account_status, :text, default: '{}' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150318194136) do 15 | 16 | create_table "users", force: :cascade do |t| 17 | t.string "name", limit: 255 18 | t.string "email", limit: 255 19 | t.string "password_digest", limit: 255 20 | t.string "publishable_key", limit: 255 21 | t.string "secret_key", limit: 255 22 | t.string "stripe_user_id", limit: 255 23 | t.string "currency", limit: 255 24 | t.datetime "created_at" 25 | t.datetime "updated_at" 26 | t.string "stripe_account_type" 27 | t.text "stripe_account_status", default: "{}" 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /docs/api-keys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfunduk/rails-stripe-connect-example/651d85a7ab1da610d163d18003c93bb6078524e9/docs/api-keys.png -------------------------------------------------------------------------------- /docs/app-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfunduk/rails-stripe-connect-example/651d85a7ab1da610d163d18003c93bb6078524e9/docs/app-setup.png -------------------------------------------------------------------------------- /docs/development-mode-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfunduk/rails-stripe-connect-example/651d85a7ab1da610d163d18003c93bb6078524e9/docs/development-mode-bar.png -------------------------------------------------------------------------------- /lib/tasks/setup.rake: -------------------------------------------------------------------------------- 1 | require 'highline/import' 2 | require 'stripe' 3 | require 'rest-client' 4 | require 'securerandom' 5 | 6 | namespace :app do 7 | desc "Setup this Rails+Stripe Connect Test Application" 8 | task :setup do 9 | say <<-EOF 10 | 11 | Thanks for trying out the 12 | <%= color("Rails Stripe Connect Test Application", BOLD) %>! 13 | ---------------------------------------------------------------- 14 | This script asks for API keys and pre-configures a bunch of 15 | things so that you don't have to tediously create/fill in 16 | settings in a bunch of files. 17 | 18 | <%= color("You should read the source of this task in lib/tasks/setup.rake!", :red, BOLD) %> 19 | ---------------------------------------------------------------- 20 | EOF 21 | 22 | confirm = %{Have you read the code about to be run and are confident it\ndoesn't do anything scary? (Y/N) } 23 | confirmed = ask confirm do |q| 24 | q.case = :up 25 | q.in = %w{ Y N } 26 | end 27 | 28 | if confirmed == 'N' 29 | puts "No worries, come back later :)" 30 | exit 1 31 | else 32 | puts 33 | end 34 | 35 | existing_config = Rails.root.join('config/secrets.yml') 36 | if File.exists?( existing_config ) 37 | # if you have an existing config/secrets.yml 38 | # we can test it for you 39 | load_config = ask "You have an existing config/secrets.yml, shall I test it? (Y/N) " do |q| 40 | q.case = :up 41 | q.in = %w{ Y N } 42 | end 43 | 44 | if load_config == 'Y' 45 | config = YAML.load_file( existing_config )['development'] 46 | client_id = config['stripe_client_id'] 47 | publishable_key = config['stripe_publishable_key'] 48 | secret_key = config['stripe_secret_key'] 49 | puts "Loaded config/secrets.yml\n\n" 50 | else 51 | puts 52 | end 53 | else 54 | # or if you like you can specify these 3 values via 55 | # environment variables instead of being prompted for them. 56 | use_env = ask "Do you want to try to load configuration from the environment? (Y/N) " do |q| 57 | q.case = :up 58 | q.in = %w{ Y N } 59 | end 60 | 61 | if use_env == 'Y' 62 | client_id = ENV['STRIPE_CLIENT_ID'] 63 | publishable_key = ENV['STRIPE_PUBLISHABLE_KEY'] 64 | secret_key = ENV['STRIPE_SECRET_KEY'] 65 | loaded = [ ('client_id' if client_id), 66 | ('publishable_key' if publishable_key), 67 | ('secret_key' if secret_key) ].compact 68 | puts "Loaded: #{'None' if loaded.empty?}#{loaded.join(', ')}\n\n" 69 | else 70 | puts 71 | end 72 | end 73 | 74 | client_id ||= ask "What is your application's development client ID? " do |q| 75 | q.validate = /ca_[a-zA-Z0-9]{14,252}/ 76 | q.echo = "*" 77 | end 78 | 79 | # with the client id, test that it's correct 80 | print "Checking client... " 81 | client_id_test_url = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id=#{client_id}" 82 | response = RestClient.get client_id_test_url 83 | if response.code != 200 84 | puts "That doesn't appear to be a valid client_id. Please see the README" 85 | exit 1 86 | else 87 | puts "OK" 88 | end 89 | 90 | # now get publishable key 91 | publishable_key ||= ask "\nWhat is your test publishable key? " do |q| 92 | q.validate = /pk_test_[a-zA-Z0-9]{24,247}/ 93 | q.echo = "*" 94 | end 95 | 96 | # check publishable 97 | print "Checking publishable... " 98 | token = Stripe::Token.create( { 99 | card: { number: '4242424242424242', 100 | exp_month: 1, exp_year: Date.today.year + 3, 101 | cvc: '123' } 102 | }, publishable_key ) rescue nil 103 | 104 | if token.nil? 105 | puts "That publishable key did not appear to work. Please see the README" 106 | exit 1 107 | else 108 | puts "OK\n" 109 | end 110 | 111 | 112 | # now get secret key 113 | secret_key ||= ask "\nWhat is your test secret key? " do |q| 114 | q.validate = /sk_test_[a-zA-Z0-9]{24,247}/ 115 | q.echo = "*" 116 | end 117 | 118 | # check secret 119 | print "Checking secret... " 120 | account = Stripe::Account.retrieve( 'self', secret_key ) rescue nil 121 | 122 | if account.nil? 123 | puts "That secret key did not appear to work. Please see the README" 124 | exit 1 125 | else 126 | puts "OK\n" 127 | end 128 | 129 | puts "\nGenerating Rails session secret key base..." 130 | rails_secret_key_base = SecureRandom.hex(64) 131 | 132 | puts "\nGenerating config/secrets.yml..." 133 | 134 | sample_path = File.join( Dir.pwd, 'config/secrets.sample.yml' ) 135 | sample_contents = File.read( sample_path ) 136 | sample_contents.gsub! '__RAILS_SECRET', rails_secret_key_base 137 | sample_contents.gsub! '__STRIPE_CLIENT', client_id 138 | sample_contents.gsub! '__STRIPE_PUBLISHABLE', publishable_key 139 | sample_contents.gsub! '__STRIPE_SECRET', secret_key 140 | 141 | dest_path = File.join( Dir.pwd, 'config/secrets.yml' ) 142 | if File.exists?( dest_path ) 143 | yn = ask "Already exists... should I overwrite it? (Y/N) " do |q| 144 | q.case = :up 145 | q.in = %w{ Y N } 146 | end 147 | exit 0 if yn == 'N' 148 | end 149 | 150 | File.write dest_path, sample_contents 151 | puts "\nDONE" 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfunduk/rails-stripe-connect-example/651d85a7ab1da610d163d18003c93bb6078524e9/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | --------------------------------------------------------------------------------