├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── app.json └── web.rb /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please only file issues here that you believe represent actual bugs in this demo code. 2 | If you're having general trouble with your Stripe integration, please email support@stripe.com for a faster response. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.6 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "2.6.6" 4 | 5 | gem 'dotenv', '2.7.4' 6 | gem 'encrypted_cookie', '0.0.5' 7 | gem 'json', '2.3.0' 8 | gem 'sinatra', '2.0.5' 9 | gem 'stripe', '4.21.2' 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | connection_pool (2.2.2) 5 | dotenv (2.7.4) 6 | encrypted_cookie (0.0.5) 7 | rack (>= 1.1, < 3) 8 | faraday (0.15.4) 9 | multipart-post (>= 1.2, < 3) 10 | json (2.3.0) 11 | multipart-post (2.1.1) 12 | mustermann (1.0.3) 13 | net-http-persistent (3.0.1) 14 | connection_pool (~> 2.2) 15 | rack (2.2.3) 16 | rack-protection (2.0.5) 17 | rack 18 | sinatra (2.0.5) 19 | mustermann (~> 1.0) 20 | rack (~> 2.0) 21 | rack-protection (= 2.0.5) 22 | tilt (~> 2.0) 23 | stripe (4.21.2) 24 | faraday (~> 0.13) 25 | net-http-persistent (~> 3.0) 26 | tilt (2.0.9) 27 | 28 | PLATFORMS 29 | ruby 30 | 31 | DEPENDENCIES 32 | dotenv (= 2.7.4) 33 | encrypted_cookie (= 0.0.5) 34 | json (= 2.3.0) 35 | sinatra (= 2.0.5) 36 | stripe (= 4.21.2) 37 | 38 | RUBY VERSION 39 | ruby 2.6.6p146 40 | 41 | BUNDLED WITH 42 | 1.17.3 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stripe 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec ruby web.rb -p $PORT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Deprecated:** This repository has moved to https://glitch.com/edit/#!/stripe-example-mobile-backend. 2 | 3 | For help with integrating the Stripe iOS and Android SDKs, including an example backend, visit our [Accept a Payment](https://stripe.com/docs/payments/accept-a-payment?platform=ios) guide. 4 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Example Stripe Backend", 3 | "description": "For testing the Stripe example mobile apps at github.com/stripe/stripe-ios and github.com/stripe/stripe-android", 4 | "repository": "https://github.com/stripe/example-mobile-backend", 5 | "keywords": ["ios", "stripe"], 6 | "env": { 7 | "STRIPE_TEST_SECRET_KEY": { 8 | "description": "Find this at https://dashboard.stripe.com/account/apikeys (it'll look like sk_test_****)", 9 | "required": true 10 | } 11 | }, 12 | "stack": "heroku-20" 13 | } 14 | -------------------------------------------------------------------------------- /web.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'stripe' 3 | require 'dotenv' 4 | require 'json' 5 | require 'encrypted_cookie' 6 | 7 | $stdout.sync = true # Get puts to show up in heroku logs 8 | 9 | Dotenv.load 10 | Stripe.api_key = ENV['STRIPE_TEST_SECRET_KEY'] 11 | 12 | use Rack::Session::EncryptedCookie, 13 | :secret => 'replace_me_with_a_real_secret_key' # Actually use something secret here! 14 | 15 | def log_info(message) 16 | puts "\n" + message + "\n\n" 17 | return message 18 | end 19 | 20 | get '/' do 21 | status 200 22 | return log_info("Great, your backend is set up. Now you can configure the Stripe example apps to point here.") 23 | end 24 | 25 | post '/ephemeral_keys' do 26 | authenticate! 27 | begin 28 | key = Stripe::EphemeralKey.create( 29 | {customer: @customer.id}, 30 | {stripe_version: params["api_version"]} 31 | ) 32 | rescue Stripe::StripeError => e 33 | status 402 34 | return log_info("Error creating ephemeral key: #{e.message}") 35 | end 36 | 37 | content_type :json 38 | status 200 39 | key.to_json 40 | end 41 | 42 | def authenticate! 43 | # This code simulates "loading the Stripe customer for your current session". 44 | # Your own logic will likely look very different. 45 | return @customer if @customer 46 | if session.has_key?(:customer_id) 47 | customer_id = session[:customer_id] 48 | begin 49 | @customer = Stripe::Customer.retrieve(customer_id) 50 | rescue Stripe::InvalidRequestError 51 | end 52 | else 53 | default_customer_id = ENV['DEFAULT_CUSTOMER_ID'] 54 | if default_customer_id 55 | @customer = Stripe::Customer.retrieve(default_customer_id) 56 | else 57 | begin 58 | @customer = create_customer() 59 | 60 | if (Stripe.api_key.start_with?('sk_test_')) 61 | # only attach test cards in testmode 62 | attach_customer_test_cards() 63 | end 64 | rescue Stripe::InvalidRequestError 65 | end 66 | end 67 | session[:customer_id] = @customer.id 68 | end 69 | @customer 70 | end 71 | 72 | def create_customer 73 | Stripe::Customer.create( 74 | :description => 'mobile SDK example customer', 75 | :metadata => { 76 | # Add our application's customer id for this Customer, so it'll be easier to look up 77 | :my_customer_id => '72F8C533-FCD5-47A6-A45B-3956CA8C792D', 78 | }, 79 | ) 80 | end 81 | 82 | def attach_customer_test_cards 83 | # Attach some test cards to the customer for testing convenience. 84 | # See https://stripe.com/docs/payments/3d-secure#three-ds-cards 85 | # and https://stripe.com/docs/mobile/android/authentication#testing 86 | ['4000000000003220', '4000000000003063', '4000000000003238', '4000000000003246', '4000000000003253', '4242424242424242'].each { |cc_number| 87 | payment_method = Stripe::PaymentMethod.create({ 88 | type: 'card', 89 | card: { 90 | number: cc_number, 91 | exp_month: 8, 92 | exp_year: 2022, 93 | cvc: '123', 94 | }, 95 | }) 96 | 97 | Stripe::PaymentMethod.attach( 98 | payment_method.id, 99 | { 100 | customer: @customer.id, 101 | } 102 | ) 103 | } 104 | end 105 | 106 | # This endpoint responds to webhooks sent by Stripe. To use it, you'll need 107 | # to add its URL (https://{your-app-name}.herokuapp.com/stripe-webhook) 108 | # in the webhook settings section of the Dashboard. 109 | # https://dashboard.stripe.com/account/webhooks 110 | # See https://stripe.com/docs/webhooks 111 | post '/stripe-webhook' do 112 | # Retrieving the event from Stripe guarantees its authenticity 113 | payload = request.body.read 114 | event = nil 115 | 116 | begin 117 | event = Stripe::Event.construct_from( 118 | JSON.parse(payload, symbolize_names: true) 119 | ) 120 | rescue JSON::ParserError => e 121 | # Invalid payload 122 | status 400 123 | return 124 | end 125 | 126 | # Handle the event 127 | case event.type 128 | when 'source.chargeable' 129 | # For sources that require additional user action from your customer 130 | # (e.g. authorizing the payment with their bank), you should use webhooks 131 | # to capture a PaymentIntent after the source becomes chargeable. 132 | # For more information, see https://stripe.com/docs/sources#best-practices 133 | source = event.data.object # contains a Stripe::Source 134 | WEBHOOK_CHARGE_CREATION_TYPES = ['bancontact', 'giropay', 'ideal', 'sofort', 'three_d_secure', 'wechat'] 135 | if WEBHOOK_CHARGE_CREATION_TYPES.include?(source.type) 136 | begin 137 | payment_intent = Stripe::PaymentIntent.create( 138 | :amount => source.amount, 139 | :currency => source.currency, 140 | :source => source.id, 141 | :payment_method_types => [source.type], 142 | :description => "PaymentIntent for Source webhook", 143 | :confirm => true, 144 | :capture_method => ENV['CAPTURE_METHOD'] == "manual" ? "manual" : "automatic", 145 | ) 146 | rescue Stripe::StripeError => e 147 | status 400 148 | return log_info("Webhook: Error creating PaymentIntent: #{e.message}") 149 | end 150 | return log_info("Webhook: Created PaymentIntent for source: #{payment_intent.id}") 151 | end 152 | when 'payment_intent.succeeded' 153 | payment_intent = event.data.object # contains a Stripe::PaymentIntent 154 | log_info("Webhook: PaymentIntent succeeded #{payment_intent.id}") 155 | # Fulfill the customer's purchase, send an email, etc. 156 | # When creating the PaymentIntent, consider storing any order 157 | # information (e.g. order number) as metadata so that you can retrieve it 158 | # here and use it to complete your customer's purchase. 159 | when 'payment_intent.amount_capturable_updated' 160 | # Capture the payment, then fulfill the customer's purchase like above. 161 | payment_intent = event.data.object # contains a Stripe::PaymentIntent 162 | log_info("Webhook: PaymentIntent succeeded #{payment_intent.id}") 163 | else 164 | # Unexpected event type 165 | status 400 166 | return 167 | end 168 | status 200 169 | end 170 | 171 | # ==== SetupIntent 172 | # See https://stripe.com/docs/payments/cards/saving-cards-without-payment 173 | 174 | # This endpoint is used by the mobile example apps to create a SetupIntent. 175 | # https://stripe.com/docs/api/setup_intents/create 176 | # A real implementation would include controls to prevent misuse 177 | post '/create_setup_intent' do 178 | payload = params 179 | if request.content_type != nil and request.content_type.include? 'application/json' and params.empty? 180 | payload = Sinatra::IndifferentHash[JSON.parse(request.body.read)] 181 | end 182 | begin 183 | setup_intent = Stripe::SetupIntent.create({ 184 | payment_method: payload[:payment_method], 185 | return_url: payload[:return_url], 186 | confirm: payload[:payment_method] != nil, 187 | customer: payload[:customer_id], 188 | use_stripe_sdk: payload[:payment_method] != nil ? true : nil, 189 | payment_method_types: payment_methods_for_country(payload[:country]), 190 | }) 191 | rescue Stripe::StripeError => e 192 | status 402 193 | return log_info("Error creating SetupIntent: #{e.message}") 194 | end 195 | 196 | log_info("SetupIntent successfully created: #{setup_intent.id}") 197 | status 200 198 | return { 199 | :intent => setup_intent.id, 200 | :secret => setup_intent.client_secret, 201 | :status => setup_intent.status 202 | }.to_json 203 | end 204 | 205 | # ==== PaymentIntent Automatic Confirmation 206 | # See https://stripe.com/docs/payments/payment-intents/ios 207 | 208 | # This endpoint is used by the mobile example apps to create a PaymentIntent 209 | # https://stripe.com/docs/api/payment_intents/create 210 | # A real implementation would include controls to prevent misuse 211 | post '/create_payment_intent' do 212 | authenticate! 213 | payload = params 214 | 215 | if request.content_type != nil and request.content_type.include? 'application/json' and params.empty? 216 | payload = Sinatra::IndifferentHash[JSON.parse(request.body.read)] 217 | end 218 | 219 | supported_payment_methods = payload[:supported_payment_methods] ? payload[:supported_payment_methods].split(",") : nil 220 | 221 | # Calculate how much to charge the customer 222 | amount = calculate_price(payload[:products], payload[:shipping]) 223 | 224 | begin 225 | payment_intent = Stripe::PaymentIntent.create( 226 | :amount => amount, 227 | :currency => currency_for_country(payload[:country]), 228 | :customer => payload[:customer_id] || @customer.id, 229 | :description => "Example PaymentIntent", 230 | :capture_method => ENV['CAPTURE_METHOD'] == "manual" ? "manual" : "automatic", 231 | payment_method_types: supported_payment_methods ? supported_payment_methods : payment_methods_for_country(payload[:country]), 232 | :metadata => { 233 | :order_id => '5278735C-1F40-407D-933A-286E463E72D8', 234 | }.merge(payload[:metadata] || {}), 235 | ) 236 | rescue Stripe::StripeError => e 237 | status 402 238 | return log_info("Error creating PaymentIntent: #{e.message}") 239 | end 240 | 241 | log_info("PaymentIntent successfully created: #{payment_intent.id}") 242 | status 200 243 | return { 244 | :intent => payment_intent.id, 245 | :secret => payment_intent.client_secret, 246 | :status => payment_intent.status 247 | }.to_json 248 | end 249 | 250 | # ===== PaymentIntent Manual Confirmation 251 | # See https://stripe.com/docs/payments/payment-intents/ios-manual 252 | 253 | # This endpoint is used by the mobile example apps to create and confirm a PaymentIntent 254 | # using manual confirmation. 255 | # https://stripe.com/docs/api/payment_intents/create 256 | # https://stripe.com/docs/api/payment_intents/confirm 257 | # A real implementation would include controls to prevent misuse 258 | post '/confirm_payment_intent' do 259 | authenticate! 260 | payload = params 261 | if request.content_type.include? 'application/json' and params.empty? 262 | payload = Sinatra::IndifferentHash[JSON.parse(request.body.read)] 263 | end 264 | 265 | begin 266 | if payload[:payment_intent_id] 267 | # Confirm the PaymentIntent 268 | payment_intent = Stripe::PaymentIntent.confirm(payload[:payment_intent_id], {:use_stripe_sdk => true}) 269 | elsif payload[:payment_method_id] 270 | # Calculate how much to charge the customer 271 | amount = calculate_price(payload[:products], payload[:shipping]) 272 | 273 | # Create and confirm the PaymentIntent 274 | payment_intent = Stripe::PaymentIntent.create( 275 | :amount => amount, 276 | :currency => currency_for_country(payload[:country]), 277 | :customer => payload[:customer_id] || @customer.id, 278 | :source => payload[:source], 279 | :payment_method => payload[:payment_method_id], 280 | :payment_method_types => payment_methods_for_country(payload[:country]), 281 | :description => "Example PaymentIntent", 282 | :shipping => payload[:shipping], 283 | :return_url => payload[:return_url], 284 | :confirm => true, 285 | :confirmation_method => "manual", 286 | # Set use_stripe_sdk for mobile apps using Stripe iOS SDK v16.0.0+ or Stripe Android SDK v10.0.0+ 287 | # Do not set this on apps using Stripe SDK versions below this. 288 | :use_stripe_sdk => true, 289 | :capture_method => ENV['CAPTURE_METHOD'] == "manual" ? "manual" : "automatic", 290 | :metadata => { 291 | :order_id => '5278735C-1F40-407D-933A-286E463E72D8', 292 | }.merge(payload[:metadata] || {}), 293 | ) 294 | else 295 | status 400 296 | return log_info("Error: Missing params. Pass payment_intent_id to confirm or payment_method to create") 297 | end 298 | rescue Stripe::StripeError => e 299 | status 402 300 | return log_info("Error: #{e.message}") 301 | end 302 | 303 | return generate_payment_response(payment_intent) 304 | end 305 | 306 | def generate_payment_response(payment_intent) 307 | # Note that if your API version is before 2019-02-11, 'requires_action' 308 | # appears as 'requires_source_action'. 309 | if payment_intent.status == 'requires_action' 310 | # Tell the client to handle the action 311 | status 200 312 | return { 313 | requires_action: true, 314 | secret: payment_intent.client_secret 315 | }.to_json 316 | elsif payment_intent.status == 'succeeded' or 317 | (payment_intent.status == 'requires_capture' and ENV['CAPTURE_METHOD'] == "manual") 318 | # The payment didn’t need any additional actions and is completed! 319 | # Handle post-payment fulfillment 320 | status 200 321 | return { 322 | :success => true 323 | }.to_json 324 | else 325 | # Invalid status 326 | status 500 327 | return "Invalid PaymentIntent status" 328 | end 329 | end 330 | 331 | # ===== Helpers 332 | 333 | # Our example apps sell emoji apparel; this hash lets us calculate the total amount to charge. 334 | EMOJI_STORE = { 335 | "👕" => 2000, 336 | "👖" => 4000, 337 | "👗" => 3000, 338 | "👞" => 700, 339 | "👟" => 600, 340 | "👠" => 1000, 341 | "👡" => 2000, 342 | "👢" => 2500, 343 | "👒" => 800, 344 | "👙" => 3000, 345 | "💄" => 2000, 346 | "🎩" => 5000, 347 | "👛" => 5500, 348 | "👜" => 6000, 349 | "🕶" => 2000, 350 | "👚" => 2500, 351 | } 352 | 353 | def price_lookup(product) 354 | price = EMOJI_STORE[product] 355 | raise "Can't find price for %s (%s)" % [product, product.ord.to_s(16)] if price.nil? 356 | return price 357 | end 358 | 359 | def calculate_price(products, shipping) 360 | amount = 1099 # Default amount. 361 | 362 | if products 363 | amount = products.reduce(0) { | sum, product | sum + price_lookup(product) } 364 | end 365 | 366 | if shipping 367 | case shipping 368 | when "fedex" 369 | amount = amount + 599 370 | when "fedex_world" 371 | amount = amount + 2099 372 | when "ups_worldwide" 373 | amount = amount + 1099 374 | end 375 | end 376 | 377 | return amount 378 | end 379 | 380 | def currency_for_country(country) 381 | # Determine currency to use. Generally a store would charge different prices for 382 | # different countries, but for the sake of simplicity we'll charge X of the local currency. 383 | 384 | case country 385 | when 'us' 386 | 'usd' 387 | when 'mx' 388 | 'mxn' 389 | when 'my' 390 | 'myr' 391 | when 'at', 'be', 'de', 'es', 'it', 'nl', 'pl' 392 | 'eur' 393 | when 'au' 394 | 'aud' 395 | when 'gb' 396 | 'gbp' 397 | when 'in' 398 | 'inr' 399 | else 400 | 'usd' 401 | end 402 | end 403 | 404 | def payment_methods_for_country(country) 405 | case country 406 | when 'us' 407 | %w[card] 408 | when 'mx' 409 | %w[card oxxo] 410 | when 'my' 411 | %w[card fpx grabpay] 412 | when 'nl' 413 | %w[card ideal sepa_debit sofort] 414 | when 'au' 415 | %w[card au_becs_debit] 416 | when 'gb' 417 | %w[card paypal bacs_debit] 418 | when 'es', 'it' 419 | %w[card paypal sofort] 420 | when 'pl' 421 | %w[card paypal p24] 422 | when 'be' 423 | %w[card paypal sofort bancontact] 424 | when 'de' 425 | %w[card paypal sofort giropay] 426 | when 'at' 427 | %w[card paypal sofort eps] 428 | when 'sg' 429 | %w[card alipay grabpay] 430 | when 'in' 431 | %w[card upi netbanking] 432 | else 433 | %w[card] 434 | end 435 | end 436 | --------------------------------------------------------------------------------